Python environment setup
2026-01-01 → 2026-01-01
Modern Python environment setup using uv.
The problem
Python’s dependency management has historically been messy. Projects need:
- Isolation — different projects need different package versions
- Reproducibility — collaborators must get the exact same environment
- Python version control — projects may require specific Python versions
Traditional tools (pip, venv, requirements.txt) don’t solve all of these well.
Modern solution: uv
uv is a fast Python package manager (written in Rust) that handles virtual environments, dependency resolution, and lockfiles in one tool.
Quick start
uv init # Initialize project (creates pyproject.toml)
uv add pandas numpy # Add dependencies
uv sync # Install everything from lockfile
That’s it. uv automatically:
- Creates
.venv/if it doesn’t exist - Generates
uv.lockwith exact versions of all dependencies (including transitive ones) - Installs everything reproducibly
Avoid mixing with pip or using uv pip — stick to uv add/uv sync to keep pyproject.toml and uv.lock in sync.
Common commands
| Command | Description |
|---|---|
uv init |
Initialize new project |
uv add PKG |
Add dependency |
uv add --dev PKG |
Add dev dependency |
uv remove PKG |
Remove dependency |
uv sync |
Install from lockfile |
uv lock |
Update lockfile without installing |
uv run CMD |
Run command in the environment |
uvx TOOL |
Run one-off tool without installing |
Project structure
myproject/
├── .python-version # Python version for this project
├── .venv/ # Virtual environment (gitignored)
├── pyproject.toml # Project metadata + dependencies
└── uv.lock # Lockfile (commit this!)
Key files
.python-version
A simple file containing the Python version:
3.12
uv reads this and uses the correct Python version. This file is understood by many tools (pyenv, uv, etc.).
pyproject.toml
The modern standard for Python project configuration (PEP 517, PEP 518). It replaces setup.py, setup.cfg, and requirements.txt.
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"pandas>=2.0",
"numpy>=1.26",
]
[project.optional-dependencies]
dev = ["pytest", "ruff"]
Why pyproject.toml is better than requirements.txt:
- Standardized — one format for all Python tools
- Structured — metadata, dependencies, and tool configs in one place
- Semantic versioning — specify version ranges, not just exact versions
- Optional dependencies — group dev/test/docs dependencies separately
uv.lock
Auto-generated lockfile with exact versions of all dependencies (including transitive). You want to commit this file — it ensures reproducibility across machines.
Auto-activation with direnv
direnv automatically activates environments when you enter a project directory.
- Add to
~/.direnvrc:
use_uv() {
[ -d .venv ] || uv venv
source .venv/bin/activate
[ -f uv.lock ] && uv sync
}
- Create
.envrcin your project:
echo "use uv" > .envrc
- Allow it:
direnv allow
Now when you cd into the project, the environment activates automatically.
Linting and formatting with ruff
ruff is an extremely fast Python linter and formatter (also written in Rust). It replaces flake8, isort, black, and many other tools.
Setup
uv add --dev ruff
Add configuration to pyproject.toml:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"] # E=pycodestyle, F=pyflakes, I=isort
Running manually
ruff check . # Lint
ruff check --fix . # Lint and auto-fix
ruff format . # Format
Format on save (editor integration)
My favorite way to use ruff is setting up “format on save” for my editors so that I don’t even need to think about formatting at all. Once you set up, every file save fixes all the formatting issues automatically.
VS Code: Install the Ruff extension and enable format on save. See VS Code setup docs.
Neovim: Install ruff via Mason, then configure with nvim-lspconfig. See Neovim setup docs.
See also
- uv
- ruff
- direnv