Python Environment and Project Management
The python community's offering of tooling for environment and project management provides semi-overlapping coverage of different use-cases and workflows. Choosing the "correct" tool depends strongly on the use-cases and workflows that one relies on for day-to-day development. This article aims to guide users in choosing which tool to use for common development tasks.
Library Summary
uv offers a simplified interface for package and project management with a universal lockfile to ensure reproducible environments. uv while being relatively new to the scene has taken the python community by storm with a value proposition of 10-100x faster dependency resolution than pip. Since then, Astral, the company backing both uv and ruff, has grown the library to cover many of the most common project management use-cases. It should be noted that Astral is a company which intends to keep its tools free and develop paid-for services on top of its open-source tooling.
Find out more in the official uv documentation
hatch is designed for creating and managing multiple environments for different use-cases, allowing for greater flexibility in project management. hatch is designed for extensibility with plugins for customizing behavior. It can be easily configured to use uv for environment creation and dependency resolution, meaning hatch users can enjoy many of uvs convenience features and optimizations while maintaining the higher degree of customization offered by hatch.
Find out more in the official hatch documentation
poetry's offering is almost purely limited to packaging and dependency management and does not contain many of the project management goodies that the other tools explored in this article offer. It is a hold-out of the previous generation of environment management tools, among the likes of pipenv and pyenv. poetry is only PEP-621 compliant for versions later than 2.0.
Find out more in the official poetry documentation
Feature Comparison
| Feature | uv | hatch | poetry |
|---|---|---|---|
| Handles python installation | yes | yes | no |
| Environment scope | one env per project1 | one env per use-case | one env per project |
| Dependency Lock | yes | no2 | yes |
| Auto-updating dependencies | yes -- complete sync must be actively prevented by user | yes -- constraint check updates only when current env violates declared dependencies | no |
| Command-line aliases | no3 | yes | no |
| Inline-metadata scripting | yes -- can add and modify dependency blocks via CLI | yes | no |
| Workspace support (shared environment across multiple projects) | yes | yes | no |
| ENVVAR configuration | no | yes | no |
Command Comparison
Create a new project
uv init --lib my-app
The --lib arg is required for the src/ directory layout.
Install environment
If a lock is present:
uv sync --all-extras
Otherwise, to create a lock:
uv venv --python <version>
uv lock
uv sync --all-extras
List environments
N/A The default location of the created environment is the .venv directory.
To configure an alternative path:
uv venv <path>
Remove environment
N/A. There is no way to programmatically removed an environment. One must manually delete .venv directory.
Change a project's python version
Configure the default python version for a project:
uv python pin <version>
Replace the current environment with a new one with a different version of python:
uv venv --python <new_version> --refresh --clear
uv sync --all-extras
List dependencies
uv pip freeze
Or create a pipdeptree visualization of the project's dependencies:
uv tree
Add a dependency
Update pyproject.toml and then either let uv run automatically update the environment before use or actively update the environment by running:
uv sync
Or add a standard dependency with the command-line:
uv add <package>
For local editable dependencies:
uv add --editable /path/to/pkg/
Add <package> to an optional-dependencies group:
uv add --optional <group_name> <package>
Add <package> to a dependency-groups group:
uv add --group <group_name> <package>
Remove a dependency
Update pyproject.toml and then either let uv run automatically update the environment before use or actively update the environment by running:
uv sync
Or remove a standard dependency with the command-line:
uv remove <package>
Remove <package> from an optional-dependencies group:
uv remove <package> --optional <group_name>
Remove <package> from a dependency-groups group:
uv remove --group <group_name> <package>
Update a dependency
uv automatically updates dependencies changed in the pyproject.toml before running commands unless told otherwise.
It may, however, be desirable to actively update dependencies, e.g. when testing release candidates or pre-releases.
To update an unpinned dependency's locked version:
uv lock --upgrade-package <package>
Or update all dependencies:
uv lock --upgrade
--upgrade and --upgrade-package both imply --refresh which updates cached data.
Run a script
Project scripts declared in pyproject.toml:
uv run <cmd> <args>
Using the uv tool or uvx interface:
uv tool run <tool> <cmd>
which is equivalent to:
uvx <tool> <cmd>
Certain tools must not be installed into the local development environment, but are instead managed by uv, e.g. uv tool run ruff --version
To learn more about the uv tool interface check out the official documentation.
Format files
Assuming the project has ruff as a dependency and ruff is configured via the pyproject.toml or a ruff.toml:
uv run ruff format <path>
Or without the declared dependency, using the uv tool interface:
uv tool run ruff format <path>
Or with uv>=0.8.13 using the experimental format command:
uv format
Run tests
uv run pytest
Footnotes
- For non-workspace usage. ↩
- PEP-751 was accepted on 01.04.2025. Since
hatchis a standards-based library, we can assume that lockfiles will be implemented inhatchin the near future. ↩ - Astral, the company behind
uv, has said that they are planning support for this feature since August 2024 ↩