Tools: Essential Guide: 10 CLAUDE.md Rules Every Python Developer Needs in 2026
10 CLAUDE.md Rules Every Python Developer Needs in 2026
Rule 1 — Type Hints Are Mandatory, Not Decorative
Rule 2 — One Lockfile, One Tool, No pip install In Prose
Rule 3 — Errors Are Specific, Never Bare
Rule 4 — Logging, Not print
Rule 5 — Tests Use pytest, Fixtures Over Setup, No Network
Rule 6 — ruff And black Are Non-Negotiable
Rule 8 — Async Is A Choice, Not An Accident
Rule 9 — Configuration Is Typed And Loaded Once
Rule 10 — Boundaries: What Claude Must Not Touch
A Minimal CLAUDE.md Skeleton
Where To Go From Here You ask Claude to "add a function that fetches a user from the API and saves to the DB" inside a Python project, and you get back: The model isn't broken. It's defaulting to the median of fifteen years of Python tutorials. Your repo doesn't look like the median of fifteen years of Python tutorials — but Claude doesn't know that until you tell it. A CLAUDE.md file at the root of your repo is the cheapest leverage you have. Claude Code reads it on every task. Cursor, Aider, and any tool that respects context files reads it too. Write the rules once, stop fighting the same fights every PR. Here are 10 rules I drop into every Python repo in 2026. Why: Without hints, Claude treats every parameter as Any. You lose IDE help, mypy can't catch regressions, and refactors become guesswork. Why: Claude will tell users to pip install foo in READMEs and forget to update pyproject.toml. Six months later nobody can reproduce the environment. If you're starting a new project in 2026, default to uv. It's fast, deterministic, and replaces pip, pip-tools, virtualenv, and pyenv in one binary. Why: except Exception: and except: are how production bugs become silent data loss. Claude reaches for them because they "make the test green." Why: print calls leak secrets, can't be silenced in tests, and don't carry structured context. Claude defaults to print because tutorials do. Why: Without rules, Claude generates unittest.TestCase classes, mocks the world, and writes tests that hit real APIs. You end up with a flaky CI suite that passes locally and fails on Tuesdays. Why: Style debates eat code review time. AI assistants generate code that "looks reasonable" but doesn't match the repo's actual conventions. Tooling enforces what humans can't. Why: Old projects scatter config across setup.py, setup.cfg, requirements.txt, tox.ini, .flake8, pytest.ini. Claude will happily add a new config file rather than consolidate. Stop the bleeding. Why: Mixing async def with blocking I/O is the silent killer of Python web apps. Claude will write async def then call requests.get() because both look like "the Python way." Why: os.environ.get("API_KEY", "") scattered across thirty files is how a missing env var becomes a 500 in prod. AI assistants love this pattern because it's what they saw most. Why: Without explicit boundaries, AI assistants "helpfully" rewrite migrations, regenerate lockfiles, or refactor legacy code that's load-bearing. Tell it where to stop. If you want to start today, drop this at the root: That alone will fix 80% of the bad code Claude generates in a fresh Python repo. These 10 rules are the foundation. The full CLAUDE.md files I run on production Python projects cover another forty rules — packaging, security headers, dependency injection patterns, structured logging schemas, CI matrix conventions, and framework-specific rules for Django, FastAPI, Flask, and Celery. If you want the whole pack — battle-tested rules across Python, TypeScript, React, Next.js, Go, Rust, Docker, and more — I've put it together as the CLAUDE.md Rules Pack. One file per stack, drop it in your repo, stop arguing with your AI. Or just steal the 10 above. They're the highest-leverage ones. The developers getting the best output from Claude in 2026 aren't the ones writing longer prompts. They're the ones writing better CLAUDE.md files. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
Type hints
- Every function signature is fully annotated, including return type.- Use `from __future__ import annotations` at the top of every module.- Prefer `list[int]`, `dict[str, User]`, `X | None` over `List`, `Dict`, `Optional`.- No `Any` without a `# type: ignore[reason]` comment explaining why.
- Public APIs are checked with `mypy --strict`.
Type hints
- Every function signature is fully annotated, including return type.- Use `from __future__ import annotations` at the top of every module.- Prefer `list[int]`, `dict[str, User]`, `X | None` over `List`, `Dict`, `Optional`.- No `Any` without a `# type: ignore[reason]` comment explaining why.
- Public APIs are checked with `mypy --strict`.
Type hints
- Every function signature is fully annotated, including return type.- Use `from __future__ import annotations` at the top of every module.- Prefer `list[int]`, `dict[str, User]`, `X | None` over `List`, `Dict`, `Optional`.- No `Any` without a `# type: ignore[reason]` comment explaining why.
- Public APIs are checked with `mypy --strict`.
Dependencies
- This project uses `uv` (or `poetry`, or `pdm` — pick one and say so).- Add deps with `uv add foo`. Never edit `pyproject.toml` by hand for versions.- The lockfile (`uv.lock`) is committed.- Never suggest `pip install` in docs, comments, or commit messages.
- Dev-only deps go under the `dev` group, not `dependencies`.
Dependencies
- This project uses `uv` (or `poetry`, or `pdm` — pick one and say so).- Add deps with `uv add foo`. Never edit `pyproject.toml` by hand for versions.- The lockfile (`uv.lock`) is committed.- Never suggest `pip install` in docs, comments, or commit messages.
- Dev-only deps go under the `dev` group, not `dependencies`.
Dependencies
- This project uses `uv` (or `poetry`, or `pdm` — pick one and say so).- Add deps with `uv add foo`. Never edit `pyproject.toml` by hand for versions.- The lockfile (`uv.lock`) is committed.- Never suggest `pip install` in docs, comments, or commit messages.
- Dev-only deps go under the `dev` group, not `dependencies`.
Error handling
- Never use bare `except:` or `except Exception:` without re-raising.- Catch the narrowest exception that fits — `KeyError`, `httpx.TimeoutException`, etc.- Re-raise with `raise CustomError(...) from e` to preserve the chain.- Domain errors live in `app/errors.py` and inherit from a single `AppError` base.
- Never `return None` on error. Raise, or return a typed `Result`.
Error handling
- Never use bare `except:` or `except Exception:` without re-raising.- Catch the narrowest exception that fits — `KeyError`, `httpx.TimeoutException`, etc.- Re-raise with `raise CustomError(...) from e` to preserve the chain.- Domain errors live in `app/errors.py` and inherit from a single `AppError` base.
- Never `return None` on error. Raise, or return a typed `Result`.
Error handling
- Never use bare `except:` or `except Exception:` without re-raising.- Catch the narrowest exception that fits — `KeyError`, `httpx.TimeoutException`, etc.- Re-raise with `raise CustomError(...) from e` to preserve the chain.- Domain errors live in `app/errors.py` and inherit from a single `AppError` base.
- Never `return None` on error. Raise, or return a typed `Result`.
Logging
- Use `logging.getLogger(__name__)` at module top. Never `print()` outside scripts in `bin/`.- Use structured logging: `log.info("user_created", extra={"user_id": user.id})`.- Never log secrets, tokens, or full request bodies.
- Configuration lives in one place (`app/logging_config.py`). Modules don't call `logging.basicConfig`.
Logging
- Use `logging.getLogger(__name__)` at module top. Never `print()` outside scripts in `bin/`.- Use structured logging: `log.info("user_created", extra={"user_id": user.id})`.- Never log secrets, tokens, or full request bodies.
- Configuration lives in one place (`app/logging_config.py`). Modules don't call `logging.basicConfig`.
Logging
- Use `logging.getLogger(__name__)` at module top. Never `print()` outside scripts in `bin/`.- Use structured logging: `log.info("user_created", extra={"user_id": user.id})`.- Never log secrets, tokens, or full request bodies.
- Configuration lives in one place (`app/logging_config.py`). Modules don't call `logging.basicConfig`.
Testing
- All tests use `pytest`. No `unittest.TestCase` classes.- Shared state lives in fixtures, not `setUp` methods.- One assertion concept per test. Use `pytest.mark.parametrize` for variants.- No real network calls. Use `respx` (httpx) or `responses` (requests) to stub.- Database tests use a real Postgres via `pytest-postgresql` or testcontainers, not mocks.
- Coverage floor: 85% on `app/`, enforced in CI.
Testing
- All tests use `pytest`. No `unittest.TestCase` classes.- Shared state lives in fixtures, not `setUp` methods.- One assertion concept per test. Use `pytest.mark.parametrize` for variants.- No real network calls. Use `respx` (httpx) or `responses` (requests) to stub.- Database tests use a real Postgres via `pytest-postgresql` or testcontainers, not mocks.
- Coverage floor: 85% on `app/`, enforced in CI.
Testing
- All tests use `pytest`. No `unittest.TestCase` classes.- Shared state lives in fixtures, not `setUp` methods.- One assertion concept per test. Use `pytest.mark.parametrize` for variants.- No real network calls. Use `respx` (httpx) or `responses` (requests) to stub.- Database tests use a real Postgres via `pytest-postgresql` or testcontainers, not mocks.
- Coverage floor: 85% on `app/`, enforced in CI.
Formatting & linting
- Format with `ruff format` (or `black`). Lint with `ruff check`.- Config lives in `pyproject.toml` under `[tool.ruff]`.- Pre-commit hook runs both. CI fails on any lint warning.- Don't disable rules per-line without a `# noqa: E501` plus reason.
- Imports are sorted by `ruff` (replaces `isort`).
Formatting & linting
- Format with `ruff format` (or `black`). Lint with `ruff check`.- Config lives in `pyproject.toml` under `[tool.ruff]`.- Pre-commit hook runs both. CI fails on any lint warning.- Don't disable rules per-line without a `# noqa: E501` plus reason.
- Imports are sorted by `ruff` (replaces `isort`).
Formatting & linting
- Format with `ruff format` (or `black`). Lint with `ruff check`.- Config lives in `pyproject.toml` under `[tool.ruff]`.- Pre-commit hook runs both. CI fails on any lint warning.- Don't disable rules per-line without a `# noqa: E501` plus reason.
- Imports are sorted by `ruff` (replaces `isort`).
Project metadata
- All tool config lives in `pyproject.toml`. No `setup.py`, no `setup.cfg`, no `requirements.txt`.- Build backend is `hatchling` (or `setuptools` if legacy). Stated explicitly.- Python version pin lives in `requires-python` and matches CI matrix.
- Entry points use `[project.scripts]`, never custom `bin/` scripts that import from src.
Project metadata
- All tool config lives in `pyproject.toml`. No `setup.py`, no `setup.cfg`, no `requirements.txt`.- Build backend is `hatchling` (or `setuptools` if legacy). Stated explicitly.- Python version pin lives in `requires-python` and matches CI matrix.
- Entry points use `[project.scripts]`, never custom `bin/` scripts that import from src.
Project metadata
- All tool config lives in `pyproject.toml`. No `setup.py`, no `setup.cfg`, no `requirements.txt`.- Build backend is `hatchling` (or `setuptools` if legacy). Stated explicitly.- Python version pin lives in `requires-python` and matches CI matrix.
- Entry points use `[project.scripts]`, never custom `bin/` scripts that import from src.
Async rules
- A function is fully `async def` (every awaited call is non-blocking) or fully sync.- Inside `async def`: no `requests`, no sync DB drivers, no `time.sleep`, no sync file I/O.- Use `httpx.AsyncClient`, `asyncio.sleep`, async SQLAlchemy, `aiofiles`.- If a sync-only library is unavoidable, wrap with `await asyncio.to_thread(fn, ...)`.
- The event loop is created by the framework — never `asyncio.run()` inside library code.
Async rules
- A function is fully `async def` (every awaited call is non-blocking) or fully sync.- Inside `async def`: no `requests`, no sync DB drivers, no `time.sleep`, no sync file I/O.- Use `httpx.AsyncClient`, `asyncio.sleep`, async SQLAlchemy, `aiofiles`.- If a sync-only library is unavoidable, wrap with `await asyncio.to_thread(fn, ...)`.
- The event loop is created by the framework — never `asyncio.run()` inside library code.
Async rules
- A function is fully `async def` (every awaited call is non-blocking) or fully sync.- Inside `async def`: no `requests`, no sync DB drivers, no `time.sleep`, no sync file I/O.- Use `httpx.AsyncClient`, `asyncio.sleep`, async SQLAlchemy, `aiofiles`.- If a sync-only library is unavoidable, wrap with `await asyncio.to_thread(fn, ...)`.
- The event loop is created by the framework — never `asyncio.run()` inside library code.
Configuration
- All env vars are loaded via a single `Settings` class (Pydantic `BaseSettings` or `dataclasses`).- The settings instance is imported, never re-read from `os.environ`.- Secrets have NO default. A missing secret must crash on startup, not at request time.- `.env.example` is committed and lists every variable. `.env` is gitignored.
- Different environments don't branch in code — they load different settings classes.
Configuration
- All env vars are loaded via a single `Settings` class (Pydantic `BaseSettings` or `dataclasses`).- The settings instance is imported, never re-read from `os.environ`.- Secrets have NO default. A missing secret must crash on startup, not at request time.- `.env.example` is committed and lists every variable. `.env` is gitignored.
- Different environments don't branch in code — they load different settings classes.
Configuration
- All env vars are loaded via a single `Settings` class (Pydantic `BaseSettings` or `dataclasses`).- The settings instance is imported, never re-read from `os.environ`.- Secrets have NO default. A missing secret must crash on startup, not at request time.- `.env.example` is committed and lists every variable. `.env` is gitignored.
- Different environments don't branch in code — they load different settings classes.
Boundaries
- Do NOT modify files in `migrations/` once they've been applied to any environment.- Do NOT edit `uv.lock` directly — use `uv add` / `uv lock`.- Do NOT refactor `app/legacy/` — migration in progress, scope it out.- Do NOT add new top-level dependencies without explicit approval.
- One logical change per commit. If a task spans modules, split it.
Boundaries
- Do NOT modify files in `migrations/` once they've been applied to any environment.- Do NOT edit `uv.lock` directly — use `uv add` / `uv lock`.- Do NOT refactor `app/legacy/` — migration in progress, scope it out.- Do NOT add new top-level dependencies without explicit approval.
- One logical change per commit. If a task spans modules, split it.
Boundaries
- Do NOT modify files in `migrations/` once they've been applied to any environment.- Do NOT edit `uv.lock` directly — use `uv add` / `uv lock`.- Do NOT refactor `app/legacy/` — migration in progress, scope it out.- Do NOT add new top-level dependencies without explicit approval.
- One logical change per commit. If a task spans modules, split it.