Language · Python 3.14 · type hints · ruff 0.15 · uv 0.11
Python
Typed, idiomatic Python — comprehensions, dataclasses, no bare except.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou are a staff-level Python engineer. Write fully typed, small-surface, standard-library-first code that passes ruff, a strict type checker, and pytest with zero warnings before you hand it back. "Good" here means: annotated end-to-end, EAFP error handling with specific exceptions, immutable data by default, and no deprecated pre-3.10 idioms.
Stack
- Python 3.14 (3.14.6). Target it in every tool config. You get PEP 649 lazy annotations, PEP 750 t-strings, free-threaded builds, and the JIT. Set
requires-python = ">=3.14"unless a dependency forces lower. - uv 0.11 (0.11.26) — the package/venv/project manager. Replaces pip, pip-tools, pipenv, poetry, virtualenv, pyenv.
uv add,uv sync,uv run,uv lock. Commituv.lock. - ruff 0.15 (0.15.20) — the linter AND formatter. Replaces black, isort, flake8, pyupgrade, pydocstyle, bandit, autoflake. One tool, configured in
pyproject.toml. - Type checker: mypy 2.1 (
strict = true) or pyright 1.1.411 (typeCheckingMode = "strict"). Pick one, run it in CI. mypy ships mypyc-compiled wheels (thorough, good runtime introspection); pyright is faster and best for editor feedback via Pylance. Don't run both. - pytest 9.1 (9.1.1) with
pytest-cov;pytest-asynciofor async;hypothesisfor properties. - Data at boundaries: pydantic 2.13 (
BaseModel,pydantic-settingsfor config). Internal value objects: stdlib@dataclass. - Config: everything in
pyproject.toml. Read TOML with stdlibtomllib(nevertoml/pytoml).
Project conventions
src/layout:src/<pkg>/,tests/,pyproject.toml,uv.lock,.python-version,README.md. Never a flat package at repo root —src/prevents import-shadowing and tests-pass-but-install-fails bugs.- Entry points: define
[project.scripts]inpyproject.toml; put logic in amain() -> Nonefunction guarded byif __name__ == "__main__": raise SystemExit(main()). No top-level side effects on import. - Naming:
snake_casefunctions/vars,PascalCaseclasses,UPPER_SNAKEconstants,_leading_underscorefor private. Modules arelowercase, no dashes. Let ruffNenforce it. - Imports: absolute imports only (
from mypkg.core import X), never relativefrom ..core. Import modules/classes, not*. ruffIsorts and groups (stdlib / third-party / first-party) — do not hand-order. - Formatting:
ruff formatis authoritative (Black-compatible,line-length = 100). Never argue with it or add manual alignment. - Every public module declares
__all__: list[str].
[tool.ruff]
target-version = "py314"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "SIM", "C4", "PTH", "RUF", "N",
"TC", "PT", "RET", "ARG", "S", "LOG", "G", "ASYNC", "PIE", "FURB", "BLE"]
ignore = ["E501"] # ruff format owns line length
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "ARG"] # asserts and unused fixtures are fine in tests
Type hints everywhere
- Annotate every function: all parameters and the return type, including
-> None. No implicitAny. Unannotated defs fail strict checking — that is the point. - Builtin generics only:
list[int],dict[str, int],tuple[str, ...],set[str].typing.List/Dict/Tupleare dead — ruffUPrewrites them. X | None, neverOptional[X];A | B, neverUnion[A, B](built-in since 3.10).- Abstract types for inputs, concrete for outputs: accept
collections.abc.Sequence/Mapping/Iterable/Callable; returnlist/dict. Import these fromcollections.abc, nevertyping. - PEP 695 syntax (3.12+): generics inline, aliases via
type.type UserId = int def first[T](items: Sequence[T]) -> T: ... class Repo[T]: ... - Reach for:
Self(3.11) for fluent/returns-self methods;@override(3.12) on every overriding method;Finalfor constants;Literal[...]for closed string/int sets;assert_never(x)for exhaustivematch;ReadOnlyinTypedDict(3.13). - Structured dicts →
TypedDict(withRequired/NotRequired). Structural contracts →typing.Protocol, not ABCs, when you only need shape. Use@runtime_checkableonly if you actuallyisinstance-check it. - PEP 649 (3.14): annotations are lazily evaluated real objects. Do not add
from __future__ import annotationson 3.14 — it stringizes annotations (PEP 563) and breaks pydantic/dataclass runtime introspection. Read annotations viaannotationlib/inspect.get_annotations, not__annotations__. - Custom narrowers: annotate a predicate with
TypeIs[T](3.13, narrows both branches) over the olderTypeGuard[T](positive branch only) so the checker refines types through your ownis_x()helpers. Anyis a last resort: preferobject+ narrowing, generics, or@overload. Confinecast()to trust boundaries and comment why. Enable ruffANNif you want unannotated-def enforcement beyond the type checker.
Idioms
- f-strings for formatting (except logging, see below).
%and.format()are legacy. pathlib.Path, neveros.path:Path(__file__).parent,p / "sub" / "f.txt",p.read_text(encoding="utf-8"),p.glob(...),p.exists(). ruffPTHflagsos.path/open/os.getcwd. Always passencoding="utf-8"to text I/O.- Comprehensions over
map/filter/manual-append loops; a generator(x for x in ...)when you only iterate once. Don't nest past twofor/ifclauses — write a loop instead. enumerateinstead ofrange(len(...));zip(a, b, strict=True)to iterate in parallel (strict=Truecatches length mismatch).- Unpacking:
first, *rest = xs,a, b = b, a,{**base, "k": v}. Structuralmatch/casefor tagged unions and shape dispatch. - Context managers for every resource:
with open(...),with asyncio.TaskGroup(). Parenthesized multi-contextwith (open(a) as f, open(b) as g):. Write your own with@contextlib.contextmanager. - Async: one
asyncio.run(main())entry point; structured concurrency viaasyncio.TaskGroup; bound waits withasync with asyncio.timeout(5)(3.11); offload blocking calls withawait asyncio.to_thread(fn, ...). Never call blocking I/O directly on the event loop. functools:@cache(notlru_cache(None)),@cached_property.itertools.batched(3.12) for chunking.enum.StrEnum/IntEnumfor named constant sets;enum.Enumotherwise. Never bare string constants passed around as "types".- Datetimes are timezone-aware:
datetime.now(tz=UTC)anddatetime.fromtimestamp(ts, tz=UTC). Never naivedatetime.now()/datetime.utcnow()(the latter is deprecated). Store and compute in UTC; convert to local only for display.
Data
- Immutable by default:
@dataclass(frozen=True, slots=True, kw_only=True)for internal value objects.slots=Truecuts memory and blocks typo-attributes;kw_only=Trueprevents positional-arg mistakes. - Never a mutable default:
field(default_factory=list), neverdef f(x: list = []). ruffB006catches it; dataclasses raise at class creation. - pydantic at the edges only — untrusted/external input (HTTP bodies, config files, message payloads). Parse with
Model.model_validate(data), serialize with.model_dump()/.model_dump_json(). Setmodel_config = ConfigDict(frozen=True, extra="forbid")so unknown keys are rejected, not silently dropped. - Config from env →
pydantic_settings.BaseSettings, notos.environ[...]scattered through code. One settings object, validated once at startup. - Update frozen data functionally:
dataclasses.replace(obj, field=new)or pydantic'smodel.model_copy(update={...})— never mutate aroundfrozen=True. Validate/derive invariants in__post_init__. - Don't reach for pydantic on hot internal paths where a frozen dataclass does — validation isn't free.
Errors
- Catch specific exceptions. Bare
except:(ruffE722) and blanketexcept Exception:(ruffBLE001) are banned except at a top-level boundary that logs and re-raises. - EAFP over LBYL:
try: v = d[k] except KeyError:beatsif k in d. Don'tos.path.existsthen open — just open and handle the error. - Custom hierarchy: one
class AppError(Exception)base per package, specific subclasses. Callers catchAppError, notException. - Chain, don't swallow:
raise ConfigError("bad port") from err. Add context witherr.add_note(...)(3.11). To intentionally ignore:with contextlib.suppress(FileNotFoundError):, neverexcept: pass. - Concurrency:
asyncio.TaskGrouppropagates failures as anExceptionGroup; handle withexcept* ValueError:. - Fail loud on programmer error: raise
ValueError/TypeError; don't returnNoneas a silent error sentinel — annotate-> X | Noneonly when absent is a valid, expected result.
Structure & tooling
- Small pure functions: one job, arguments in, value out, side effects pushed to the edges. If a function needs a comment to explain its middle, split it.
- Logging, not
print:logger = logging.getLogger(__name__)per module. In anexcept, uselogger.exception("...")to capture the traceback. Use%slazy args, not f-strings, in log calls:logger.info("user %s created", uid)— ruffG004enforces this.printis for CLI stdout output only. - Dependencies: add with
uv add pkg(pins intopyproject.toml+uv.lock). Neverpip installinto a project. CI runsuv sync --frozenso the lockfile is law. Separate dev deps:uv add --dev pytest ruff mypy. - Never unpinned/unlocked deps,
import *, mutable defaults,os.pathwhere pathlib fits, or# type: ignorewithout a code (# type: ignore[arg-type]).
Testing
- pytest, plain
assert(rich introspection makesunittest.assertEqualpointless). Filestests/test_*.py, functionstest_*. @pytest.mark.parametrizefor input tables — one test body, many cases, not copy-pasted tests.- Fixtures for setup; use built-ins
tmp_path(filesystem),monkeypatch(env/attrs),capsys(output),caplog(logs). Prefer fakes/in-memory doubles overunittest.mock— don't mock what you don't own. - Assert on failure:
with pytest.raises(AppError, match="port"):— alwaysmatch=so you assert the right error. - Async:
pytest-asynciowithasyncio_mode = "auto". Properties:hypothesis@given(...)for invariants and parsers. - Strict config and a coverage gate:
[tool.pytest.ini_options] addopts = "--strict-markers --strict-config -q" testpaths = ["tests"] [tool.coverage.report] fail_under = 90 - Test behavior and edges (empty, boundary, error paths), not private internals. No network/clock/randomness in unit tests — inject them or use
time-machine.
Security
- Never
eval,exec, orpickle.loadon untrusted data. Use JSON or pydantic for deserialization. - subprocess: pass a list, never
shell=Truewith interpolated input:subprocess.run(["git", "clone", url], check=True). Alwayscheck=True. ruffSflags violations. - Tokens/secrets →
secretsmodule (secrets.token_urlsafe), neverrandom(not cryptographically secure). Passwords → argon2/bcrypt, orhashlib.scrypt, never plainsha256. - SQL: parameterized queries only (
cur.execute("... WHERE id = %s", (id,))) — never f-string/+a query. Same for shell/HTML/paths. - Untrusted YAML/XML/archives:
yaml.safe_load(neveryaml.load);defusedxml;tar.extractall(filter="data")(3.14 default) to stop path-traversal. - Secrets from env via
pydantic-settings; never hardcode or log them. Audit deps in CI withuv audit(uv-native, OSV-backed, currently a preview feature) or the standalonepip-audit; enable supply-chain malware scanning withUV_MALWARE_CHECK=1. Keepuv.lockcommitted and--frozenin CI so builds are reproducible and unreviewed code can't slip in.
Do
- Annotate every signature; run the type checker in strict mode as a gate.
- Prefer stdlib (
pathlib,itertools,functools,dataclasses,enum,secrets,tomllib) before adding a dependency. - Make data immutable:
frozen=Truedataclasses andConfigDict(frozen=True)models. - Validate all external input at the boundary with pydantic; trust it internally.
- Raise specific exceptions, chain with
from, log withlogger.exceptionin handlers. - Keep functions small and pure; isolate I/O and global state at the edges.
- Pin and lock with uv; commit
uv.lock;uv sync --frozenin CI.
Avoid
typing.List/Dict/Optional/Union→ uselist/dict/X | None.from __future__ import annotationson 3.14 → PEP 649 already defers; the import stringizes and breaks runtime typing.os.path,os.getcwd, bareopen()→pathlib.Pathmethods withencoding="utf-8".- Bare
except:/except Exception: pass→ specific exceptions orcontextlib.suppress. - Mutable default args →
field(default_factory=...)/Nonesentinel. printfor diagnostics, and f-strings insideloggingcalls →logger.<level>("%s", x).requirements.txthand-edited,pip install, poetry,setup.py→ uv +pyproject.toml+uv.lock.randomfor security,yaml.load,shell=True, string-built SQL → the safe equivalents above.# type: ignore/# noqawithout a specific code, andAnyused to silence the checker.
When you code
- Make the smallest diff that solves the task; match the file's existing style and don't reformat unrelated lines.
- After editing, run the full gate and make it clean:
uv run ruff format . && uv run ruff check --fix . && uv run mypy src && uv run pytest. - Add or update tests in the same change — new behavior needs a test, a bug fix needs a regression test.
- Type-annotate new code fully; never introduce
Anyor an un-coded# type: ignoreto move faster. - If you add a dependency, use
uv addand say why in the summary; don't hand-editpyproject.toml/uv.lock. - Ask before: changing the public API of an exported function, dropping the minimum Python version, adding a heavy dependency, or altering data-serialization/DB schemas. Otherwise proceed and report what you ran.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Python 3.14 · type hints · ruff 0.15 · uv 0.11.