Modern Python Project Setup with uv
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
uvcommands, 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 | shuv is a single static binary — no Python installation is required beforehand; uv can install and manage Python itself.
2. Scaffold the project
| Command | When to use it |
|---|---|
uv init myproject | A general project — script(s) or an app, not meant to be imported elsewhere. |
uv init --lib myproject | A library you intend to import or publish — adds a proper src/myproject/__init__.py package. |
uv init --package myproject | Forces 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.12Writes .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__.pyThe 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
| Command | Effect |
|---|---|
uv add requests | Runtime dependency — needed to run the project. |
uv add --dev pytest ruff mypy | Dev-only tooling — not installed for consumers of the package. |
uv add --group docs mkdocs | A 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 = true8. 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: false10. Locking for reproducible installs
uv.lock pins exact versions of every dependency (direct and transitive) and should be committed to version control.
| Command | Use case |
|---|---|
uv lock | Update the lockfile after editing pyproject.toml by hand. |
uv sync --locked | CI/CD: install exactly what’s locked, and fail if the lockfile is stale rather than silently updating it. |
uv lock --upgrade | Deliberately 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 tooluv 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"