Python Atlas
Tooling, web & data

uv, pip, and packaging

Create reproducible environments, define pyproject metadata, and ship importable Python packages.

uv, pip, and packaging

A Python project is reproducible when another developer can create an isolated environment, install the intended dependency graph, import the package, and run its commands without guessing.

pip and uv solve different-sized problems

pip installs packages. It does not create environments, choose Python interpreters, or define a universal lockfile workflow. Those jobs traditionally require venv, pip-tools, and other tools.

uv provides an integrated workflow:

  • install and select Python versions;
  • create and manage .venv;
  • add, remove, resolve, and lock dependencies;
  • install the current project;
  • run commands inside the project environment;
  • build and publish distributions.

Use pip when compatibility or a constrained environment requires it. Prefer uv for a new application when you want one fast, consistent interface.

Start a package

uv init --package weather-tools
cd weather-tools
uv add httpx
uv add --dev pytest ruff mypy

A useful src layout is:

weather-tools/
├── pyproject.toml
├── README.md
├── src/
│   └── weather_tools/
│       ├── __init__.py
│       └── cli.py
└── tests/
    └── test_cli.py

The src layout prevents tests from accidentally importing the repository directory instead of the installed package. It catches missing package data and build configuration earlier.

Understand pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "weather-tools"
version = "0.1.0"
description = "Weather data utilities"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
  "httpx>=0.27,<1",
]

[project.optional-dependencies]
postgres = ["sqlalchemy[asyncio]>=2.0,<3", "psycopg[binary]>=3.2,<4"]

[project.scripts]
weather = "weather_tools.cli:main"

[dependency-groups]
dev = [
  "mypy>=1.10",
  "pytest>=8",
  "ruff>=0.5",
]

[tool.hatch.build.targets.wheel]
packages = ["src/weather_tools"]

Important boundaries:

  • [build-system] tells build frontends how to create wheels and source distributions.
  • [project] contains standardized package metadata and runtime dependencies.
  • [project.optional-dependencies] exposes installable features to package consumers.
  • [dependency-groups] describes local development groups; it is not package metadata.
  • [project.scripts] creates executable commands without handwritten launcher scripts.

Avoid declaring the same dependency in multiple places. Runtime imports belong in dependencies; test and lint tools belong in a development group.

Applications and libraries pin differently

For an application, commit uv.lock and deploy from it:

uv lock
uv sync --locked

--locked fails if pyproject.toml and the lockfile disagree, which is appropriate in CI.

A reusable library should usually declare compatible ranges because its dependencies must coexist with the consuming application. Test the lowest and highest supported Python/dependency combinations when compatibility is important. A lockfile still makes the library's own development environment repeatable, but consumers resolve from the declared metadata.

Run tools without activating

uv run weather --help
uv run pytest
uv run python -m weather_tools.cli

uv run makes scripts and automation less dependent on shell activation state. If you prefer manual activation:

# POSIX shells
source .venv/bin/activate

# Windows PowerShell
.venv\Scripts\Activate.ps1

Equivalent pip workflow

python -m venv .venv

# POSIX
. .venv/bin/activate

# Windows PowerShell
.venv\Scripts\Activate.ps1

python -m pip install --upgrade pip
python -m pip install -e ".[postgres]"
python -m pip install pytest ruff mypy

Use python -m pip, not a bare pip, when interpreter ambiguity is possible. Editable installation keeps import behavior package-like while reflecting source changes.

pip freeze > requirements.txt records an environment, including indirect dependencies and local accidents. It is useful for some application deployments, but it is not a substitute for curated package metadata. Never paste a frozen environment into [project.dependencies].

Build and inspect distributions

uv build
uvx twine check dist/*

Test the wheel in a clean environment:

uv venv --python 3.12 .wheel-test
uv pip install --python .wheel-test dist/*.whl
.wheel-test/bin/python -c "import weather_tools"

On Windows, the final executable is .wheel-test\Scripts\python.exe.

Package design guidance

  • Keep import-time work minimal. Do not open network or database connections in __init__.py.
  • Expose a small public API; internal modules may change without breaking consumers.
  • Use relative imports within one package and absolute imports across top-level packages.
  • Include non-Python assets explicitly through the selected build backend.
  • Derive versions from one source. Duplicated versions drift.
  • Separate configuration from secrets; never package .env files.

Common pitfalls

“It works from the repository root”

The current directory is on sys.path, so a missing installation can remain hidden. Run tests against an installed package and test a built wheel before release.

Unbounded or exact library dependencies

No upper bound can admit a future breaking release; exact pins make dependency resolution hostile for consumers. Choose ranges based on upstream compatibility guarantees and your test matrix.

Import package name differs from distribution name

Hyphens in weather-tools become underscores in import weather_tools. Document both names where users install and import the package.

Updating without reviewing

uv lock --upgrade
uv tree
uv run pytest

Review lockfile changes and release notes. “Latest” is a moving set of behavior.

Practice

  1. Create a src-layout package with one console script.
  2. Add a runtime dependency and a development-only dependency; locate each in pyproject.toml.
  3. Build a wheel and import it from a clean environment outside the repository.
  4. Add one optional feature and verify both base and feature installations.
  5. Intentionally make the lockfile stale and observe uv sync --locked fail.

LEARNING RECORD

Finish this lesson

Mark it complete when you can explain the main decision without looking.

KNOWLEDGE CHECK

uv, pip, and packaging

1A reusable library imports HTTPX at runtime, offers optional PostgreSQL support, and uses pytest only while developing. Where should these dependencies be declared?
2Which release check most directly catches a package that imports successfully from the repository root but is missing files in its wheel?