Python rules for AI coding agents
General Python is where agents look most fluent and still make old mistakes: mutable default arguments, bare except:, subprocess.run with shell=True, floats for money, or requests inside an async def. The Python rules apply to any Python 3 code and the FastAPI rules are meant to be used on top of them.
They start by making the agent read requires-python in pyproject.toml and write for the lowest version it allows, so it does not reach for syntax the project cannot run. From there they cover typing every public function, catching the narrowest exception you can handle, using pathlib instead of string paths, a src/ layout with import-time side effects kept out, secrets from the environment, and pytest with plain assert and fixtures.
Folder: rules/python/ · python.mdc, CLAUDE.md, AGENTS.md
What the Python rules tell your agent
- Version and toolingRead
requires-pythonin pyproject.toml and write code for the lowest version it allows. For Python 3.10+ useX | Noneandmatch; for 3.12+ thetype Alias = ...statement and PEP 695 generics (def first[T](xs: list[T]) -> T). - IdiomsUse pathlib (
Path("data") / "in.csv") instead of string joins andos.path. - TypingAnnotate every public function's parameters and return type. Accept abstract types (
Iterable,Mapping,Sequencefromcollections.abc) and return concrete ones. - ErrorsCatch the narrowest exception you can handle. Never write bare
except:and do not catchExceptionjust to log and continue. - AsyncUse
asyncio.run(main())as the single entry point. Do not create event loops by hand.
First rule of each section in python.mdc, quoted as shipped.
Mistakes these rules are written to prevent
- Mutable default arguments:
def f(items=[])shares one list across calls. UseNoneand create inside. - Late binding in closures: lambdas in a loop capture the variable, not the value. Bind with a default arg or
functools.partial. iscompares identity. Use==for values; reserveisforNone,True,Falseand sentinels.
Source: “Pitfalls to avoid”.
Checks the agent runs before it says done
Example commands from the Python file (swap in your repo's own):
- Lint:
ruff check . - Format check:
ruff format --check . - Types:
mypy src(orpyright) - Tests:
pytest -q
Run ruff check, ruff format --check, the type checker and pytest (or the project's task runner equivalents) and report the results. If you could not run a command, say so plainly instead of implying it passed.
Before and after: Python: exception handling review
Prompt card 08 · Stack: Python 3 · Type: Review and refactor · Best model: a frontier reasoning model
What the card is for
Replace bare and over-broad except blocks, lost tracebacks and silent return None failures with narrow handling, chained exceptions and domain errors that callers can act on.
Worked example from the card
Before
def get_rate(currency):
try:
resp = requests.get(f"{RATES_URL}/{currency}")
data = resp.json()
rate = float(data["rate"])
cache[currency] = rate
return rate
except:
print("rate lookup failed")
return None
Expected AI output (abridged)
| 2-9 | bare except | narrow handling | catches KeyboardInterrupt and real bugs (KeyError on a
| | | | typo) and hides them as "None".
Exceptions possible: requests.RequestException (network, timeout), ValueError from .json()
or float(), KeyError for a missing "rate".
- Network errors: expected; translate to RateUnavailable so callers can fall back.
- Bad payload: the provider broke its contract; translate to RateUnavailable with the cause.
- No timeout was set; requests waits indefinitely. Added timeout=5.
- HTTP errors (500) were treated as success. Added raise_for_status().
Caller change: checkout.py tested `if rate is None`; it now catches RateUnavailable.
After
log = logging.getLogger(__name__)
class RateUnavailable(Exception):
"""The exchange rate could not be fetched or parsed."""
def get_rate(currency: str) -> Decimal:
try:
resp = requests.get(f"{RATES_URL}/{currency}", timeout=5)
resp.raise_for_status()
payload = resp.json()
except requests.RequestException as err:
raise RateUnavailable(f"rate service unreachable for {currency}") from err
try:
rate = Decimal(str(payload["rate"]))
except (KeyError, TypeError, InvalidOperation) as err:
log.warning("bad rate payload for %s: %r", currency, payload)
raise RateUnavailable(f"malformed rate for {currency}") from err
cache[currency] = rate
return rate
Excerpt from card 08; the prompt itself is in the pack.
More Python prompt cards in the pack
- Card 09: Python: add type hints to an untyped module
- Card 10: Python: refactor a script into testable functions
Using these rules in Cursor, Claude Code and AGENTS.md tools
Cursor
Put _base.mdc and python.mdc in .cursor/rules/. Globs: pyproject.toml, **/*.py.
Claude Code
Root CLAUDE.md, base first. Path-scoped variant .claude/rules/python.md for **/*.py, pyproject.toml; PostToolUse hook: ruff format <file>.
AGENTS.md tools (Codex, Gemini CLI, Aider)
Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in the repo root, or each Python package directory. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.