The distinction that matters
A dependency declaration describes what an application needs. A resolved installation records the specific package versions selected to satisfy those needs. Production reliability depends on being able to recreate and verify that installation.
The name of a configuration format does not guarantee reproducibility. A requirements specification can contain loose version ranges, and a project declaration can pin direct dependencies while leaving transitive dependencies unresolved.
Declare intent, resolve versions deliberately, and deploy the combination you tested.
Three responsibilities
| Responsibility | Typical mechanism | Question it answers |
|---|---|---|
| Declaration | Project metadata and dependency constraints | What does the application need? |
| Resolution | A lockfile or fully pinned requirements | Which versions will be installed? |
| Installation | An environment synchronization or installation tool | What is actually present at runtime? |
Python’s pyproject.toml provides a standard place for project metadata and dependency declarations. A lockfile adds a resolved dependency graph. Neither replaces the Python interpreter, operating-system libraries, or runtime configuration.
Three workable approaches
Pinned requirements with pip
Keep direct dependencies as inputs and generate pinned requirements, including transitive dependencies. Hash verification can make installation more explicit about which distributions are accepted.
This works well when an established deployment already uses pip. Avoid turning an unrelated development environment’s entire package list into the production specification.
Project metadata with pip-tools
Use project metadata as the declaration and generate a requirements specification for installation. This keeps project configuration together while preserving a pip-based deployment process.
Generated output should be regenerated from its inputs. Editing both independently creates two competing sources of truth.
Project metadata with uv
Use uv to resolve dependencies, maintain a lockfile, and synchronize the application environment. Development and deployment can share one workflow, with different dependency groups selected for each purpose.
The following commands assume a uv project whose dependencies and development group are already declared:
# Resolve intentionally during development.
uv lock
# Synchronize development dependencies; reject an outdated lockfile.
uv sync --locked
# Install the production selection without the development group.
uv sync --locked --no-dev --no-editable
An extra and a dependency group are different concepts. An optional feature extra must be selected explicitly; a development group follows the tool’s group-selection rules. Naming an extra dev does not turn it into a development group.
Locking is only part of reproducibility
An installation also depends on the Python version, operating system, architecture, available package distributions, and build tools. Native libraries and GPU runtimes can add further constraints.
Use the deployment target’s environment in CI. When deploying containers, test an image and promote that same image rather than rebuilding independently for every environment.
Dependency updates should happen as reviewed changes. Re-resolving the latest compatible packages during every deployment makes releases harder to explain and recover.
A practical decision rule
- Keep a reliable pip workflow when it already provides pinned, verified installs.
- Use project metadata with generated requirements when improving configuration without changing the deployment interface.
- Consider a project-and-lockfile workflow when establishing a new development and deployment process.
- Keep an existing team standard when it already meets these requirements.
uv manages dependency environments. Uvicorn serves web applications. A command that runs Uvicorn through uv combines those roles; it does not make them the same tool.