A Python application may work correctly on one developer's machine but fail on another. The cause is often a difference in installed package versions, Python interpreters, or dependency resolution.
For example, a project may work with one version of FastAPI but fail after a dependency update introduces an incompatible change. If developers install packages directly without recording the resolved versions, reproducing the original environment becomes difficult.
The Python tool uv helps manage this problem by handling project dependencies, virtual environments, and lockfiles in one workflow. It can resolve dependencies, record exact package versions, and install the locked dependencies in development and CI/CD environments.
This article explains how to use uv to manage a Python project, generate a lockfile, install reproducible dependencies, and avoid common dependency management mistakes.
What Is uv?
uv is a Python package and project management tool. It supports dependency resolution, virtual environment management, package installation, and Python version management.
A traditional workflow might use pip with a manually maintained requirements.txt file. This can work well, especially when versions are pinned, but developers may need separate tools or commands to manage environments and Python versions.
uv provides a unified workflow for these tasks.
For example, a project can declare its dependencies in pyproject.toml and use a uv.lock file to record the resolved dependency graph.
These files serve different purposes:
pyproject.tomldeclares the project's metadata and dependency requirements.uv.lockrecords the resolved package versions and related information needed to reproduce the dependency selection.The virtual environment contains the packages installed for the project.
A lockfile helps make dependency installation consistent, but it does not guarantee that every aspect of a build will be identical. Python versions, operating systems, environment variables, external services, and native libraries can still affect application behavior.
Step 1: Install uv
Install uv using an installation method approved for your development environment. For example, on a system with Python and pip available, you can install it with:
python -m pip install uvFor a shared development environment or production build system, follow your organization's tool installation and version management policy.
Verify that uv is available:
uv --versionThe command prints the installed uv version.
For reproducible CI/CD pipelines, use a controlled uv version rather than allowing the tool to change unexpectedly between builds. This helps reduce differences in dependency resolution and build behavior.
Step 2: Create a Python Project
You can start a new project with uv:
uv init my-python-app
cd my-python-appThe generated project includes a pyproject.toml file and an initial Python application file. The exact generated files depend on the project type and uv version.
A simplified pyproject.toml might look like this:
[project]
name = "my-python-app"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []The requires-python field describes the Python versions supported by the project. Choose a range that matches the versions you actually test and support.
The dependencies field lists the packages required by the application. Development tools and test dependencies can be managed separately so they do not automatically become runtime requirements.
For an existing Python project, you do not need to create a new application. You can initialize uv support in the project directory and adapt the dependency configuration to your existing structure.
Step 3: Add Dependencies
Use uv add to declare an application dependency.
For example:
uv add fastapi
uv add uvicornThese commands update the project's dependency configuration and resolve the required packages.
The resulting pyproject.toml might contain:
[project]
name = "my-python-app"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi",
"uvicorn"
]The dependency declarations do not need to pin every package to an exact version. Instead, the project can express acceptable version ranges while the lockfile records the selected versions.
This separation is useful because the application requirements describe which versions are acceptable, while the lockfile captures the versions selected for a particular resolved dependency graph.
To add development dependencies, use a development dependency group:
uv add --dev pytestThis keeps the testing tool separate from the application's regular runtime dependencies.
Step 4: Generate and Understand uv.lock
When you add dependencies or run the appropriate project commands, uv creates or updates uv.lock.
You can explicitly resolve the project's dependencies with:
uv lockThe lockfile records the resolved versions of direct and transitive dependencies. Transitive dependencies are packages required by other packages rather than declared directly by your application.
For example, your application may depend on FastAPI, which in turn depends on other packages. Those indirect dependencies also influence the application's behavior and must be considered when reproducing the environment.
A simplified project structure looks like this:
my-python-app/
├── pyproject.toml
├── uv.lock
├── .python-version
├── .venv/
└── main.pyThe .venv directory contains the local virtual environment and normally should not be committed to source control. The .python-version file, when used, can record the project's selected Python version for uv to use.
The uv.lock file should normally be committed to source control for an application project. It allows other developers and CI systems to use the same resolved dependency set instead of independently selecting potentially different versions.
Do not edit the lockfile manually. Change dependencies through the project configuration and uv commands, then review the generated lockfile changes.
Step 5: Install Dependencies from the Lockfile
To install the project's dependencies, use:
uv syncThis creates or updates the project's virtual environment and installs the dependencies defined by the project and lockfile.
For a CI/CD build that must not change the lockfile, use:
uv sync --lockedThe --locked option requires the lockfile to be up to date with the project configuration. If the lockfile needs to change, the command fails instead of silently updating it.
This is useful in automated builds because an outdated lockfile becomes a visible problem that developers must fix in source control.
You can also run a project command inside the managed environment:
uv run python main.pyThe uv run command ensures that the project's environment is prepared before running the command. It can synchronize the environment when needed, so in CI environments where lockfile immutability matters, explicitly run uv sync --locked first.
For example:
uv sync --locked
uv run --locked pytestThis checks that the lockfile matches the project configuration and runs the tests using the project's managed environment.
Step 6: Use uv in GitHub Actions
A CI workflow should install a controlled uv version, set up the required Python version, install locked dependencies, and run tests.
For example:
name: Python Tests
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.9.0"
- name: Set up Python
run: uv python install 3.13
- name: Install locked dependencies
run: uv sync --locked
- name: Run tests
run: uv run --locked pytestThe uv and Python versions shown are examples. Select versions supported by your project and pin the CI tooling according to your maintenance policy. In production, review third-party GitHub Actions and consider pinning them to verified full commit SHAs rather than relying only on mutable version tags.
This workflow checks out the repository, installs uv, prepares Python, synchronizes dependencies without changing the lockfile, and runs the tests.
If uv.lock is missing or inconsistent with pyproject.toml, the locked synchronization step fails. Developers can resolve the dependency configuration locally, review the lockfile change, and commit both files together.
For faster pipelines, caching uv's package downloads may reduce repeated network requests. Treat caching as a performance optimization: the workflow should still succeed when the cache is empty.
Step 7: Update Dependencies Safely
A lockfile should not prevent dependency updates forever. Packages need updates for security fixes, bug fixes, and compatibility changes.
Use uv to update dependencies deliberately.
To update the lockfile within the version constraints already declared by the project, run:
uv lock --upgradeReview the resulting uv.lock changes and run the application's test suite before merging them.
If a package needs a particular version range, adjust its declaration in pyproject.toml and resolve the lockfile again.
Avoid updating all dependencies directly in a production environment without testing. A lockfile makes the selected versions explicit, but it cannot guarantee that an updated package is compatible with your application's assumptions.
For larger projects, use a regular dependency update process that includes automated tests, security scanning, and review of important package changes.
Common Mistakes to Avoid
Committing pyproject.toml without uv.lock
For an application that relies on a lockfile, committing only the project configuration allows different environments to resolve different compatible versions.
Commit both files and review them together.
Updating the lockfile during CI
If CI silently changes the lockfile, a successful build may not correspond to the dependency state recorded in source control.
Use uv sync --locked and other appropriate locked commands to make configuration drift visible.
Assuming a lockfile fixes every build difference
The lockfile controls dependency resolution, but Python versions, native libraries, operating systems, environment variables, and external services can still change the result.
Control these inputs where reproducibility matters.
Using incompatible Python versions
A dependency may not support every Python version allowed by your project. Choose a supported Python version and test the versions your application claims to support.
Treating cached packages as the source of truth
A package cache speeds up installation but should not determine which dependencies the application uses. Keep dependency declarations and the lockfile as the authoritative project configuration.
Best Practices for Production Projects
Commit pyproject.toml and uv.lock together. Use locked synchronization in CI/CD, and keep the Python and uv versions controlled.
Separate runtime dependencies from development tools when appropriate. Review dependency updates before deployment, and run tests against the locked environment.
If your application uses native extensions or platform-specific packages, test on the operating systems and architectures where it will run. A consistent lockfile does not remove platform-specific differences.
Finally, make dependency updates part of normal maintenance. A lockfile is most useful when it provides a repeatable baseline that can be updated and reviewed deliberately.
Conclusion
uv provides a practical way to manage Python dependencies and make application builds more consistent. The combination of pyproject.toml and uv.lock records the project's requirements and resolved dependency versions.
Use uv add to manage dependencies, uv lock to resolve them, and uv sync --locked to install them without allowing CI to rewrite the lockfile. Then run tests using the same controlled environment.
These practices reduce unexpected dependency changes between developer machines and CI/CD pipelines while making package updates easier to review and reproduce.
Join the conversation! Your thoughts help the community grow.