Python Development
Use uv for everything. No pip, no virtualenv, no poetry, no pipx.
Stack
Select what's needed firstly from: typer + rich + textual + platformdirs + pydantic-settings + structlog + tomllib + httpx + keyring + watchfiles + pytest + uv + ruff + mypy
Python version
Use the current stable release. At time of writing that's 3.13; 3.12 remains a solid choice. uv will pin the version in .python-version — commit that file and keep the team in sync. Mirror this version in pyproject.toml, ruff, and mypy config.
Project setup
uv init myproject
cd myproject
uv syncuv init myproject
cd myproject
uv syncThis gives you pyproject.toml, .python-version, and a managed .venv. Don't touch the venv directly.
Running things
Always uv run. Never activate the venv manually.
uv run python main.py
uv run pytest -v
uv run ruff check .uv run python main.py
uv run pytest -v
uv run ruff check .Correct Python version and dependencies, every time.
Dependencies
uv add httpx # runtime dep
uv add --group dev ruff pytest # dev dep
uv remove somelib # remove
uv sync # reinstall from lockfileuv add httpx # runtime dep
uv add --group dev ruff pytest # dev dep
uv remove somelib # remove
uv sync # reinstall from lockfileuv.lock is committed. requirements.txt is not used.
Global CLI tools
uv tool install pre-commit
uv tool install repomixuv tool install pre-commit
uv tool install repomixNot pip install --user or pipx. uv tool manages isolated environments per tool.
Inline scripts
For standalone scripts with their own dependencies:
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///
import httpx# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///
import httpxuv run script.pyuv run script.pyFor project CLIs, use [project.scripts] in pyproject.toml.
Dev tooling
Ruff — formatting and linting
Replaces black, isort, flake8, and most of pylint. One tool.
uv run ruff format . # format
uv run ruff check . # lint
uv run ruff check --fix # lint + autofixuv run ruff format . # format
uv run ruff check . # lint
uv run ruff check --fix # lint + autofixStarter config in pyproject.toml:
[tool.ruff]
target-version = "py312" # keep in sync with .python-version
line-length = 88
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort (import sorting)
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"RUF", # ruff-specific
][tool.ruff]
target-version = "py312" # keep in sync with .python-version
line-length = 88
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort (import sorting)
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"RUF", # ruff-specific
]Solid baseline without noise. Add "S" (security), "PT" (pytest style), "D" (docstrings) as the project matures.
Mypy — static type checking
Worth adding for anything with an API surface or shared library code. Not every script needs it.
uv add --group dev mypy
uv run mypy src/uv add --group dev mypy
uv run mypy src/[tool.mypy]
python_version = "3.12" # keep in sync with .python-version
check_untyped_defs = true
warn_return_any = true[tool.mypy]
python_version = "3.12" # keep in sync with .python-version
check_untyped_defs = true
warn_return_any = trueStart permissive and tighten as coverage improves. check_untyped_defs = true is the single most useful non-strict setting — it typechecks function bodies even without annotations.
Pytest
uv add --group dev pytest pytest-cov
uv run pytest -v
uv run pytest --cov=src/ --cov-report=term-missinguv add --group dev pytest pytest-cov
uv run pytest -v
uv run pytest --cov=src/ --cov-report=term-missing[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"Keep tests in tests/ mirroring src/ structure. Prefer plain functions over test classes.
Project layout
myproject/
├── pyproject.toml
├── uv.lock
├── .python-version
├── justfile
├── README.md
├── docs/
│ └── backlog.md
├── src/
│ └── myproject/
│ ├── __init__.py
│ └── main.py
└── tests/
└── test_main.pymyproject/
├── pyproject.toml
├── uv.lock
├── .python-version
├── justfile
├── README.md
├── docs/
│ └── backlog.md
├── src/
│ └── myproject/
│ ├── __init__.py
│ └── main.py
└── tests/
└── test_main.pyUse src/ layout. It prevents accidental imports from the project root and makes packaging unambiguous.
pyproject.toml skeleton
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12" # pin to current stable; update when you start the project
dependencies = []
[dependency-groups]
dev = ["ruff", "pytest", "pytest-cov", "mypy"]
[tool.ruff]
target-version = "py312" # keep in sync with .python-version
line-length = 88
[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "SIM", "RUF"]
[tool.mypy]
python_version = "3.12" # keep in sync with .python-version
check_untyped_defs = true
warn_return_any = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12" # pin to current stable; update when you start the project
dependencies = []
[dependency-groups]
dev = ["ruff", "pytest", "pytest-cov", "mypy"]
[tool.ruff]
target-version = "py312" # keep in sync with .python-version
line-length = 88
[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "SIM", "RUF"]
[tool.mypy]
python_version = "3.12" # keep in sync with .python-version
check_untyped_defs = true
warn_return_any = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"What not to do
- Don't
pip install. Useuv addoruv tool install. - Don't activate the venv. Use
uv run. - Don't create
requirements.txt. The lockfile isuv.lock. - Don't use
setup.pyorsetup.cfg. Everything is inpyproject.toml. - Don't use black or isort separately. Ruff handles both.
- Don't start with
mypy --stricton existing code. Tighten incrementally.