Modern Python skill for Claude Code
uv, ruff, strict types, one pyproject.toml. Claude builds Python projects the 2025 way: uv manages interpreters, venvs and the lockfile, dev tools live in dependency groups, ruff replaces black, isort and flake8, and new code ships with strict type hints using 3.12 generics. It migrates requirements.txt, poetry or setup.py projects without breaking installs and knows when to reach for dataclasses versus Pydantic.
- name
python-modern - size220 lines · ≈2,150 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Sets up and writes modern Python 3.12+ projects — uv for environments and locking, pyproject.toml with dependency groups, ruff for lint and format, strict type checking with PEP 695 generics, dataclasses vs Pydantic, src layout, and packaging to PyPI. Use when starting a Python project, migrating from pip/requirements.txt/poetry/setup.py, adding type hints, configuring ruff or mypy, or writing scripts with inline dependencies. Try asking
- Migrate this requirements.txt and setup.py project to uv and pyproject.toml
- Set up ruff and mypy strict for this repo and fix what they flag
- Write a one-file script that fetches this API and needs httpx, runnable with no venv setup
Use it when
- Starting a new Python package, CLI, or service
- Moving off pip, poetry, pipenv, or setup.py
- Adding type hints or a type checker to existing code
- Configuring lint, format, and test tooling in pyproject.toml
Skip it when
- Building a FastAPI service end to end — use fastapi-service
- Speeding up slow Python code — use python-profiling
- Organizing a pytest suite and fixtures — use pytest-suites
How to check the result
On a fresh clone, uv sync --locked, ruff check, ruff format --check, the type checker, and pytest all pass with no manual venv activation.
The common mistake it prevents
Putting pytest, ruff and mypy in optional-dependencies so they ship in published metadata, instead of PEP 735 dependency groups.
Done well means: one pyproject.toml describes the project, uv.lock pins it,
uv run works on a fresh clone, ruff check and ruff format --check pass, the type
checker runs in strict mode on new code, and nobody activates a virtualenv by hand.
Ground rules
- Use uv for Python versions, venvs, dependencies, locking, tools, and builds. Do not
mix pip installs into a uv-managed
.venv. - Commit
uv.lockfor applications and libraries alike; CI installs with--locked. - Target the oldest Python you support in
requires-python, then let ruff’sUPrules and the type checker enforce syntax for that version. - Types on every public function signature; validate untrusted data at the edge with Pydantic (or msgspec), use plain dataclasses inside.
- Never edit
uv.lockby hand. Never pin with==in a library’s dependencies.
Inspect before changing
ls pyproject.toml setup.py setup.cfg requirements*.txt poetry.lock Pipfile uv.lock 2>/dev/null
python3 --version; uv --version
grep -nE "requires-python|build-backend|\[tool\." pyproject.toml
| Found | Action |
|---|---|
requirements.txt only | uv init --bare, then uv add -r requirements.txt, move test/lint deps with uv add --dev |
poetry.lock / [tool.poetry] | Convert metadata to PEP 621 [project]; uvx migrate-to-uv automates most of it, review the diff |
setup.py | Move metadata to [project]; keep setup.py only if a C extension build needs it |
uv.lock present | Use uv sync / uv add; never pip install |
New project workflow
uv init --package myapp # src/ layout, build backend, console script
cd myapp
uv python pin 3.13 # writes .python-version
uv add httpx pydantic
uv add --dev pytest ruff mypy # goes to [dependency-groups] dev
uv run pytest
uv run python -m myapp
- Library:
uv init --lib. App that is never installed as a package:uv init --app. - Global CLI tools:
uv tool install ruffor one-offuvx ruff check. - Lock-only changes:
uv lock --upgrade-package httpx; everything:uv lock --upgrade. - CI install:
uv sync --locked(fails if the lock is stale) — never a bareuv sync. - Export for platforms that need it:
uv export --format requirements-txt --no-hashes > requirements.txt.
For a full annotated pyproject.toml (ruff, mypy, pytest, coverage settings), read
references/pyproject-template.md when creating or
overhauling the config.
Single-file scripts (PEP 723)
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx>=0.27", "rich"]
# ///
import httpx
from rich import print
print(httpx.get("https://example.org").status_code)
Run with uv run fetch.py; add deps with uv add --script fetch.py typer. Prefer this
over a throwaway venv for utilities and one-off data jobs.
Typing: modern syntax
from collections.abc import Callable, Iterable, Mapping, Sequence
from typing import Self, override, TypedDict, NotRequired
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None # 3.12 alias
def first[T](items: Sequence[T], default: T | None = None) -> T | None: # 3.12 generics
return items[0] if items else default
class Repo[ModelT]:
def get(self, id: int) -> ModelT | None: ...
class Builder:
def with_name(self, name: str) -> Self: ...
class Child(Base):
@override # type checker errors if Base has no such method
def save(self) -> None: ...
class Event(TypedDict):
id: str
payload: NotRequired[dict[str, object]]
Rules:
X | None,list[int],dict[str, int]— neverOptional,List,Dict.- Accept abstract (
Iterable,Mapping,Sequence), return concrete (list,dict). - Callbacks:
Callable[[int, str], bool]; for complex signatures use aProtocolwith__call__. - Structural typing with
typing.Protocolinstead of ABCs for dependency seams. TypeIs(3.13, ortyping_extensions) for narrowing helpers; prefer it toTypeGuard.- Only add
from __future__ import annotationson 3.12/3.13 if you need forward refs everywhere; 3.14 evaluates annotations lazily by default.
Type checker choice: mypy --strict is the conservative default; pyright (strict mode)
is faster and stricter on inference. Pick one per repo and run it in CI. Astral’s ty
is promising but check its release status before relying on it as the only gate.
Data modeling decision
| Need | Use |
|---|---|
| Internal value object, trusted data | @dataclass(slots=True, frozen=True, kw_only=True) |
| Parse/validate untrusted JSON, env, API bodies | Pydantic v2 BaseModel / pydantic-settings |
| Very hot decode path, simple schemas | msgspec Struct |
| Dict shape for a JSON payload you only pass through | TypedDict |
| Fixed set of values | enum.StrEnum / enum.IntEnum |
from dataclasses import dataclass, field
from datetime import datetime, UTC
@dataclass(slots=True, frozen=True, kw_only=True)
class Invoice:
id: str
amount_cents: int
tags: tuple[str, ...] = ()
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
Never use a mutable default (tags: list[str] = []); dataclasses reject it, plain
functions silently share it.
Language features worth reaching for (3.11+)
matchfor structural dispatch on tagged dicts/dataclasses, not as a longifchain over plain values.asyncio.TaskGroupinstead of baregatherfor structured concurrency; failures cancel siblings and surface asExceptionGroup(except* ValueError:).tomllib(read-only TOML) in the stdlib;zoneinfoinstead of pytz.datetime.now(UTC)— never naivedatetime.utcnow()(deprecated in 3.12).pathlib.Pathoveros.path;Path.read_text(encoding="utf-8")always with encoding.- f-string debugging
f"{value=}"; 3.12 allows nested quotes in f-strings.
Ruff configuration essentials
[tool.ruff]
line-length = 100
target-version = "py312" # inferred from requires-python if omitted
[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP", "SIM", "RUF", "C4", "PTH", "DTZ", "S", "N", "PL", "TID"]
ignore = ["PLR0913", "S101"] # many args ok; assert ok (tests)
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S", "PLR2004"]
Commands: uv run ruff check --fix ., uv run ruff format .. Ruff replaces flake8,
isort, pyupgrade, and black; remove their configs when adopting it.
Packaging and publishing
uv build # sdist + wheel in dist/
uvx twine check dist/* # metadata sanity
uv publish # uses trusted publishing in GitHub Actions, or UV_PUBLISH_TOKEN
- Build backend:
hatchlingis the safe default;uv_buildis fine for pure-Python projects on current uv. - Version single source:
[project] versionor a dynamic version from VCS (hatch-vcs); not both. - Ship a
py.typedmarker in the package for typed libraries. - Prefer PyPI trusted publishing (OIDC) over long-lived tokens.
Gotchas
- A
src/layout prevents tests from importing the working tree accidentally; keep it. uv runsyncs the environment first; in CI useuv run --lockedoruv sync --lockedthenuv run --no-sync.requires-python = ">=3.12"without an upper bound is correct;<4caps cause resolver pain for downstream users.- Dev tools in
[project.optional-dependencies]leak into published metadata; use[dependency-groups](PEP 735) instead. except Exception: passis never acceptable; ruff’sS110/BLErules catch it.- Free-threaded (no-GIL) builds exist (3.13t, supported in 3.14); C extensions must declare support or the GIL is re-enabled at import. Do not promise speedups untested.
Verification
uv sync --locked
uv run ruff check . && uv run ruff format --check .
uv run mypy src # or: uv run pyright
uv run pytest -q
uv build # for packages: wheel builds cleanly
All five must pass on a fresh clone. If uv sync --locked fails, run uv lock, review
the lock diff, and commit it separately from code changes.
Output for a migration
**From:** requirements.txt + setup.py **To:** pyproject.toml + uv.lock
**Python:** requires-python >=3.12, pinned 3.13 for dev
**Tooling:** ruff (replaced black, isort, flake8), mypy --strict on src/
**Commands:** `uv sync`, `uv run pytest`, `uv run ruff check --fix .`
**Follow-ups:** 41 mypy errors in legacy/ excluded via per-module override Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/pyproject-template.md
# Annotated pyproject.toml template
Use as a starting point; delete sections the project does not need. Values assume a
`src/` layout package named `myapp`.
```toml
[project]
name = "myapp"
version = "0.1.0"
description = "One sentence that says what this does."
readme = "README.md"
requires-python = ">=3.12"
license = "MIT" # PEP 639 SPDX expression
authors = [{ name = "Your Name", email = "[email protected]" }]
dependencies = [
"httpx>=0.27", # lower bounds only for libraries
"pydantic>=2.7",
]
[project.scripts]
myapp = "myapp.cli:main" # console entry point
[project.urls]
Homepage = "https://github.com/you/myapp"
[dependency-groups] # PEP 735: never published
dev = ["pytest>=8", "pytest-cov", "ruff", "mypy"]
docs = ["mkdocs-material"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# ---------------- uv ----------------
[tool.uv]
default-groups = ["dev"] # groups synced by default
# ---------------- ruff ----------------
[tool.ruff]
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP", "SIM", "RUF", "C4", "PTH", "DTZ", "S", "N", "PL", "TID", "PT"]
ignore = ["PLR0913"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "PLR2004"]
[tool.ruff.lint.isort]
known-first-party = ["myapp"]
[tool.ruff.format]
docstring-code-format = true
# ---------------- mypy ----------------
[tool.mypy]
strict = true
python_version = "3.12"
warn_unreachable = true
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = ["legacy.*"] # quarantine, shrink over time
ignore_errors = true
[[tool.mypy.overrides]]
module = ["some_untyped_lib.*"]
ignore_missing_imports = true
# ---------------- pytest ----------------
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = ["-ra", "--strict-markers", "--strict-config", "--import-mode=importlib"]
filterwarnings = ["error"] # warnings fail tests; add targeted ignores
# ---------------- coverage ----------------
[tool.coverage.run]
source = ["myapp"]
branch = true
[tool.coverage.report]
show_missing = true
skip_covered = true
exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError", "\\.\\.\\."]
```
## pyright alternative to mypy
```toml
[tool.pyright]
include = ["src", "tests"]
typeCheckingMode = "strict"
pythonVersion = "3.12"
reportMissingTypeStubs = false
```
## GitHub Actions job
```yaml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python }}
enable-cache: true
- run: uv sync --locked
- run: uv run ruff check . && uv run ruff format --check .
- run: uv run mypy src
- run: uv run pytest --cov
```
Check the latest major tag of `astral-sh/setup-uv` before pinning; pin to a commit SHA
in security-sensitive repos.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.