Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Language · Python 3.14 · type hints · ruff 0.15 · uv 0.11

Python

Typed, idiomatic Python — comprehensions, dataclasses, no bare except.

pythontype-hintsruff

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You 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. Commit uv.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-asyncio for async; hypothesis for properties.
  • Data at boundaries: pydantic 2.13 (BaseModel, pydantic-settings for config). Internal value objects: stdlib @dataclass.
  • Config: everything in pyproject.toml. Read TOML with stdlib tomllib (never toml/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] in pyproject.toml; put logic in a main() -> None function guarded by if __name__ == "__main__": raise SystemExit(main()). No top-level side effects on import.
  • Naming: snake_case functions/vars, PascalCase classes, UPPER_SNAKE constants, _leading_underscore for private. Modules are lowercase, no dashes. Let ruff N enforce it.
  • Imports: absolute imports only (from mypkg.core import X), never relative from ..core. Import modules/classes, not *. ruff I sorts and groups (stdlib / third-party / first-party) — do not hand-order.
  • Formatting: ruff format is 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 implicit Any. Unannotated defs fail strict checking — that is the point.
  • Builtin generics only: list[int], dict[str, int], tuple[str, ...], set[str]. typing.List/Dict/Tuple are dead — ruff UP rewrites them.
  • X | None, never Optional[X]; A | B, never Union[A, B] (built-in since 3.10).
  • Abstract types for inputs, concrete for outputs: accept collections.abc.Sequence/Mapping/Iterable/Callable; return list/dict. Import these from collections.abc, never typing.
  • 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; Final for constants; Literal[...] for closed string/int sets; assert_never(x) for exhaustive match; ReadOnly in TypedDict (3.13).
  • Structured dictsTypedDict (with Required/NotRequired). Structural contractstyping.Protocol, not ABCs, when you only need shape. Use @runtime_checkable only if you actually isinstance-check it.
  • PEP 649 (3.14): annotations are lazily evaluated real objects. Do not add from __future__ import annotations on 3.14 — it stringizes annotations (PEP 563) and breaks pydantic/dataclass runtime introspection. Read annotations via annotationlib/inspect.get_annotations, not __annotations__.
  • Custom narrowers: annotate a predicate with TypeIs[T] (3.13, narrows both branches) over the older TypeGuard[T] (positive branch only) so the checker refines types through your own is_x() helpers.
  • Any is a last resort: prefer object + narrowing, generics, or @overload. Confine cast() to trust boundaries and comment why. Enable ruff ANN if you want unannotated-def enforcement beyond the type checker.

Idioms

  • f-strings for formatting (except logging, see below). % and .format() are legacy.
  • pathlib.Path, never os.path: Path(__file__).parent, p / "sub" / "f.txt", p.read_text(encoding="utf-8"), p.glob(...), p.exists(). ruff PTH flags os.path/open/os.getcwd. Always pass encoding="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 two for/if clauses — write a loop instead.
  • enumerate instead of range(len(...)); zip(a, b, strict=True) to iterate in parallel (strict=True catches length mismatch).
  • Unpacking: first, *rest = xs, a, b = b, a, {**base, "k": v}. Structural match/case for tagged unions and shape dispatch.
  • Context managers for every resource: with open(...), with asyncio.TaskGroup(). Parenthesized multi-context with (open(a) as f, open(b) as g):. Write your own with @contextlib.contextmanager.
  • Async: one asyncio.run(main()) entry point; structured concurrency via asyncio.TaskGroup; bound waits with async with asyncio.timeout(5) (3.11); offload blocking calls with await asyncio.to_thread(fn, ...). Never call blocking I/O directly on the event loop.
  • functools: @cache (not lru_cache(None)), @cached_property. itertools.batched (3.12) for chunking.
  • enum.StrEnum/IntEnum for named constant sets; enum.Enum otherwise. Never bare string constants passed around as "types".
  • Datetimes are timezone-aware: datetime.now(tz=UTC) and datetime.fromtimestamp(ts, tz=UTC). Never naive datetime.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=True cuts memory and blocks typo-attributes; kw_only=True prevents positional-arg mistakes.
  • Never a mutable default: field(default_factory=list), never def f(x: list = []). ruff B006 catches 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(). Set model_config = ConfigDict(frozen=True, extra="forbid") so unknown keys are rejected, not silently dropped.
  • Config from envpydantic_settings.BaseSettings, not os.environ[...] scattered through code. One settings object, validated once at startup.
  • Update frozen data functionally: dataclasses.replace(obj, field=new) or pydantic's model.model_copy(update={...}) — never mutate around frozen=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: (ruff E722) and blanket except Exception: (ruff BLE001) are banned except at a top-level boundary that logs and re-raises.
  • EAFP over LBYL: try: v = d[k] except KeyError: beats if k in d. Don't os.path.exists then open — just open and handle the error.
  • Custom hierarchy: one class AppError(Exception) base per package, specific subclasses. Callers catch AppError, not Exception.
  • Chain, don't swallow: raise ConfigError("bad port") from err. Add context with err.add_note(...) (3.11). To intentionally ignore: with contextlib.suppress(FileNotFoundError):, never except: pass.
  • Concurrency: asyncio.TaskGroup propagates failures as an ExceptionGroup; handle with except* ValueError:.
  • Fail loud on programmer error: raise ValueError/TypeError; don't return None as a silent error sentinel — annotate -> X | None only 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 an except, use logger.exception("...") to capture the traceback. Use %s lazy args, not f-strings, in log calls: logger.info("user %s created", uid) — ruff G004 enforces this. print is for CLI stdout output only.
  • Dependencies: add with uv add pkg (pins into pyproject.toml + uv.lock). Never pip install into a project. CI runs uv sync --frozen so the lockfile is law. Separate dev deps: uv add --dev pytest ruff mypy.
  • Never unpinned/unlocked deps, import *, mutable defaults, os.path where pathlib fits, or # type: ignore without a code (# type: ignore[arg-type]).

Testing

  • pytest, plain assert (rich introspection makes unittest.assertEqual pointless). Files tests/test_*.py, functions test_*.
  • @pytest.mark.parametrize for 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 over unittest.mock — don't mock what you don't own.
  • Assert on failure: with pytest.raises(AppError, match="port"): — always match= so you assert the right error.
  • Async: pytest-asyncio with asyncio_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, or pickle.load on untrusted data. Use JSON or pydantic for deserialization.
  • subprocess: pass a list, never shell=True with interpolated input: subprocess.run(["git", "clone", url], check=True). Always check=True. ruff S flags violations.
  • Tokens/secretssecrets module (secrets.token_urlsafe), never random (not cryptographically secure). Passwords → argon2/bcrypt, or hashlib.scrypt, never plain sha256.
  • 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 (never yaml.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 with uv audit (uv-native, OSV-backed, currently a preview feature) or the standalone pip-audit; enable supply-chain malware scanning with UV_MALWARE_CHECK=1. Keep uv.lock committed and --frozen in 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=True dataclasses and ConfigDict(frozen=True) models.
  • Validate all external input at the boundary with pydantic; trust it internally.
  • Raise specific exceptions, chain with from, log with logger.exception in 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 --frozen in CI.

Avoid

  • typing.List/Dict/Optional/Union → use list/dict/X | None.
  • from __future__ import annotations on 3.14 → PEP 649 already defers; the import stringizes and breaks runtime typing.
  • os.path, os.getcwd, bare open()pathlib.Path methods with encoding="utf-8".
  • Bare except: / except Exception: pass → specific exceptions or contextlib.suppress.
  • Mutable default args → field(default_factory=...) / None sentinel.
  • print for diagnostics, and f-strings inside logging calls → logger.<level>("%s", x).
  • requirements.txt hand-edited, pip install, poetry, setup.py → uv + pyproject.toml + uv.lock.
  • random for security, yaml.load, shell=True, string-built SQL → the safe equivalents above.
  • # type: ignore / # noqa without a specific code, and Any used 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 Any or an un-coded # type: ignore to move faster.
  • If you add a dependency, use uv add and say why in the summary; don't hand-edit pyproject.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.

Back to top ↑