Alex Garbiak
← Lib

Modern Python Project Setup with uv

Updated pythonuvsetupcheatsheet

A general-purpose baseline for now — the plan is to trim this down to the setup I actually use, and drop the rest. For day-to-day uv commands, see the Python uv Command Reference.

uv replaces the usual pile of pip, venv, pip-tools, pipx, and pyenv with one tool and one lockfile, so a “modern” setup is mostly about letting uv own the whole dependency lifecycle instead of hand-rolling it.

1. Install uv

curl -LsSf https://astral.sh/uv/install.sh | sh

uv is a single static binary — no Python installation is required beforehand; uv can install and manage Python itself.

2. Scaffold the project

CommandWhen to use it
uv init myprojectA general project — script(s) or an app, not meant to be imported elsewhere.
uv init --lib myprojectA library you intend to import or publish — adds a proper src/myproject/__init__.py package.
uv init --package myprojectForces a packaged layout with a build backend, without the “library” defaults.

This creates pyproject.toml, a src/ layout, .python-version, and a README.md, and initializes uv.lock.

3. Pin the Python version

uv python pin 3.12

Writes .python-version. Every uv run / uv sync in this project then uses that version, downloading it automatically via uv python install if it isn’t already available — no separate pyenv needed.

4. Project layout

myproject/
├── .python-version
├── pyproject.toml
├── uv.lock
├── README.md
└── src/
    └── myproject/
        └── __init__.py

The src/ layout (rather than a flat myproject/ next to pyproject.toml) forces imports to go through the installed package, catching “works by accident because of the current directory” bugs early.

5. Dependencies & groups

CommandEffect
uv add requestsRuntime dependency — needed to run the project.
uv add --dev pytest ruff mypyDev-only tooling — not installed for consumers of the package.
uv add --group docs mkdocsA named, opt-in group (e.g. for docs builds), separate from dev.

These are recorded under [project.dependencies] and [dependency-groups] in pyproject.toml, and resolved together into uv.lock.

6. Linting & formatting — Ruff

Ruff replaces Flake8, isort, and (for formatting) Black with one fast tool.

uv add --dev ruff
uv run ruff check .      # lint
uv run ruff format .     # format
# pyproject.toml
[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

7. Type checking

uv add --dev mypy
uv run mypy src
[tool.mypy]
python_version = "3.12"
strict = true

8. Testing — pytest

uv add --dev pytest
uv run pytest
[tool.pytest.ini_options]
testpaths = ["tests"]

9. Pre-commit hooks

Run the checks above automatically before every commit.

uv add --dev pre-commit
uv run pre-commit install
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: ruff-check
        name: ruff check
        entry: uv run ruff check .
        language: system
        pass_filenames: false
      - id: ruff-format
        name: ruff format
        entry: uv run ruff format --check .
        language: system
        pass_filenames: false

10. Locking for reproducible installs

uv.lock pins exact versions of every dependency (direct and transitive) and should be committed to version control.

CommandUse case
uv lockUpdate the lockfile after editing pyproject.toml by hand.
uv sync --lockedCI/CD: install exactly what’s locked, and fail if the lockfile is stale rather than silently updating it.
uv lock --upgradeDeliberately bump everything to the latest allowed versions.

11. Running things

uv run pytest              # run inside the project's environment
uv run python -m myproject # run a module
uv run ruff check .        # run a dev tool

uv run syncs the environment to match the lockfile first, so there’s no separate “did I activate the venv?” step.

12. Building & publishing

uv build              # produces dist/*.whl and dist/*.tar.gz
uv publish            # uploads to PyPI (or --index for a private registry)

Minimal end-to-end example

uv init --lib myproject
cd myproject
uv python pin 3.12
uv add requests
uv add --dev ruff mypy pytest pre-commit
uv run pre-commit install
git init && git add . && git commit -m "chore: scaffold project"