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:

  1. Isolation — different projects need different package versions
  2. Reproducibility — collaborators must get the exact same environment
  3. 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:

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:

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.

  1. Add to ~/.direnvrc:
use_uv() {
    [ -d .venv ] || uv venv
    source .venv/bin/activate
    [ -f uv.lock ] && uv sync
}
  1. Create .envrc in your project:
echo "use uv" > .envrc
  1. 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

×