- all nine files: trailing "Related" sections removed, their unique links inlined where the topic actually arises (FFI boundary -> c-cpp, embedded frontend -> js-typescript, build scripts -> bash, etc.) - README conventions: rule 2 mandated a Related Skills section, which now contradicts practice — replaced with inline-linking and one-home-per-fact rules. The <repo>/<rel path> rule was appended after the wrong section last commit; moved into the list where it belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.0 KiB
name, description
| name | description |
|---|---|
| python | Use this skill when writing, linting, type-checking, testing, or packaging Python — including async code, CLI tools, bot cogs, and library packages. Covers strict typing with mypy, ruff configuration, virtualenv discipline, async patterns, and packaging. |
Python — Rules & Preferences
Extends coding-dev.md. Everything there applies; this adds Python specifics.
For the surrounding build and deploy scripts, see bash.md.
1. The Gate
ruff format --check .
ruff check .
mypy --strict .
pytest
All four clean before anything is called done. Configure them in pyproject.toml so the
editor, the CLI, and CI all read the same settings.
2. Typing — Strict
Every function gets annotated, including -> None. Type hints are not optional
documentation; they are the checker's input.
[tool.mypy]
strict = true
warn_unreachable = true
warn_return_any = true
disallow_untyped_defs = true
- Never bare
Any. Use aProtocol, aTypeVar, orobject+ narrowing. X | NoneoverOptional[X];list[str]overList[str](modern syntax, notypingimport).TypedDictor a dataclass for structured dicts — notdict[str, Any].LiteralandEnumfor closed sets of values, not bare strings.@dataclass(frozen=True, slots=True)for value objects.- Third-party library missing stubs: install
types-*if it exists, otherwise a narrowly scoped# type: ignore[import-untyped]with a comment — never a blanket module-level ignore.
3. Linting
ruff is the linter and formatter (it replaces flake8, isort, black, pyupgrade, and most
plugins). Enable a broad rule set rather than the default minimum:
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF", "ASYNC", "S", "PTH"]
S (bandit security rules) and ASYNC are worth the noise. As always: fix, don't suppress —
and when you must suppress, use a specific code (# noqa: S603) with a reason.
4. Environments
- Always a virtualenv or project-managed environment (
uv,poetry,pdm,venv). Never install into the system interpreter, and neverpip install --useras a workaround for an immutable OS — use a container or a venv. uvis the preferred installer/resolver where free choice exists; it is dramatically faster and lockfile-native.- Commit the lockfile. Pin the Python version in
pyproject.toml(requires-python). pathlib.Pathoveros.patheverywhere (ruff'sPTHrules enforce this).
5. Errors & Logging
- Specific exceptions only.
except Exception:needs a reason; bareexcept:is never acceptable (it catchesKeyboardInterruptandSystemExit). - Never swallow silently. If you catch and continue, log at
warningor above. raise ... from errto preserve the chain.- Custom exception types deriving from a single package-level base, so callers can catch the whole family.
loggingmodule, neverprint, in anything that isn't a CLI writing to stdout as its actual output. Onelogger = logging.getLogger(__name__)per module, at module scope.- Never log secrets — tokens, auth headers, message content, personal data.
6. Async
asynciowithasync/await. Never block the event loop — notime.sleep, no synchronousrequests, no blocking file I/O in a coroutine. Useasyncio.to_threadfor genuinely blocking calls.httpx(or the framework's own client) overrequestsin async code.asyncio.TaskGroup(3.11+) over barecreate_task— it propagates exceptions and cancels siblings instead of losing them to an unawaited task.- Every
create_taskresult must be held in a reference, or the garbage collector can cancel it mid-flight. - Cancellation is cooperative: clean up in
finally, and never swallowCancelledError. - Events over polling. Subscribe to the library's event/callback API; do not loop on a timer. Where polling is genuinely unavoidable (no event source exists), use a staggered backoff rather than a fixed interval, and say why polling was necessary.
7. Rate Limiting & External Services
When talking to a rate-limited API (a chat platform, a store, a tracker):
- Funnel outbound calls through a single queue/worker rather than calling from arbitrary call sites. It is the only place a limit can actually be enforced.
- Backoff with jitter on retry; never a tight retry loop.
- Respect
Retry-Afterand documented limits; assume undocumented ones exist. - Be extremely careful scraping real consumer services. Conservative delays, realistic headers, plenty of failsafes, and abort fast when a response looks unexpected. Assume you are one mistake away from a ban.
8. Structure
- Package layout:
src/<package>/, tests intests/. - Shared behavior goes in a real shared module, imported by each consumer — not copy-pasted between sibling packages. When three components grow the same queue/retry/ formatting logic, that is the signal to extract it.
- Reusable, generic functionality (a format parser, a protocol client) becomes its own installable package with its own repo and version, consumed as a dependency.
- Keep persistent state in the framework's own config/storage system so it survives restarts; do not invent a parallel JSON file beside it.
- File and function size limits from the general rules apply.
9. Testing
pytest, withpytest-asynciofor coroutines.- Fixtures over setup/teardown methods;
tmp_pathfor anything touching the filesystem. - Regression test every fixed bug.
- Mock at the network boundary (
respx,responses), not by monkeypatching your own internals — a test that patches the code under test proves nothing. - Never exercise destructive operations against real accounts or real data. Temporary fixtures only.
pytest -p no:randomlyonly when order genuinely matters, and note why.