Binding style rules for Python projects, target Python 3.12+ (with 3.13 features noted where relevant). Prioritize correctness, explicitness, simplicity — never cleverness, never abstraction for its own sake.
This guide extends and defers to:
- PEP 8 — Python's canonical style guide.
- PEP 20 (The Zen of Python) — the worldview ("Explicit is better than implicit. Simple is better than complex. Errors should never pass silently.").
- PEP 484 + PEP 604 + PEP 695 — type hints, union syntax, modern generics.
- Google Python Style Guide — docstring conventions and additional taste.
Where our guidance conflicts with PEP 8 or PEP 20, the PEPs win. This guide adds project-specific conventions: ruthless type hints, Tiger-style discipline (50-line function cap, assertion density, bounded loops), structured concurrency via asyncio.TaskGroup, and a strong preference for @dataclass(frozen=True, slots=True) + Protocol over inheritance.
- Clarity — code's purpose is clear to the reader.
- Simplicity — the simplest approach that accomplishes the goal.
- Concision — high signal-to-noise ratio.
- Maintainability — easy to modify correctly over time.
- Consistency — matches the surrounding codebase.
Resolve rule conflicts in this order. Consistency is the tiebreaker, never an override.
| # | Document | Scope |
|---|---|---|
| 01 | Formatting & Tooling | Ruff (lint + format), mypy strict, line length, function-size cap, pre-commit |
| 02 | Naming Conventions | snake_case, PascalCase, _private, __dunder, module names |
| 03 | Type Hints | Type every public signature, | over Optional, Protocol for structural typing, no Any in public API |
| 04 | Variables & Declarations | Mutable default args, Final, ClassVar, walrus, module-level constants |
| 05 | Functions | Keyword-only args, default args, decorators, functools, single-purpose, 50-line cap |
| 06 | Classes & Data Modeling | @dataclass(frozen=True, slots=True), Protocol, ABCs sparingly, enum, no inheritance for reuse |
| 07 | Pythonic Idioms | Context managers, generators, comprehensions, EAFP, dunders, pathlib, f-strings, match/case |
| 08 | Error Handling | Custom exception hierarchies, exception chaining, no bare except, contextlib.suppress, fail fast |
| 09 | Concurrency & Async | asyncio.TaskGroup, asyncio.timeout, cancellation, threading only when forced, GIL realities |
| 10 | API Design | Protocol interfaces, keyword-only public APIs, __all__, deprecation, semver discipline |
| 11 | Testing | pytest, fixtures, @pytest.mark.parametrize, hypothesis, no shared state, pytest-asyncio |
| 12 | Package Organization | src/ layout, pyproject.toml, __init__.py, no cyclic imports |
| 13 | Resource Management | with/async with, contextlib, asyncio cancellation, timeouts, secrets/os.urandom |
| 14 | Documentation | Google-style docstrings, examples, type hints document what docstrings don't |
| 15 | Performance | Profile first, generators, __slots__, functools.lru_cache, asyncio cost, GIL, no premature opt |
Security, performance, and git practices are covered in the root-level code style guide. The cross-cutting docs are language-agnostic; this guide adapts them to Python.
Correctness > performance > developer experience. When they conflict, this ordering decides.
Python's design ("There should be one — and preferably only one — obvious way to do it") is closer to our principles than most languages. The Zen of Python is most of this guide. The rest is discipline overlays that the language doesn't enforce but a serious codebase needs.
- Lean on dataclasses + Protocols. Avoid inheritance for code reuse. Python uses classes well — and we use them.
@dataclass(frozen=True, slots=True)for state,Protocolfor behavior contracts, plain functions and methods for transformations. Inheritance is reserved for shared interface (and even there,Protocolusually wins), not for sharing implementation. Composition + duck typing + Protocols cover the rest. - Types are part of the contract. Type-hint every public signature. Run mypy in strict mode. No
Anyin public API. UseProtocolfor structural typing — Python's duck typing made type-safe. - Immutable by default.
frozen=Trueon dataclasses.tupleoverlistwhen the contents don't change. No mutable default arguments. Mutability is an explicit choice you have to type — that's the right way around. - Explicit over implicit. No global state. No module-level side effects in imports. No
from foo import *outside of explicit re-exports. Every dependency in the function signature or constructor. Magic is for libraries; applications are explicit. - Exceptions are the mechanism — handle every one explicitly. Python's idiom is exceptions, and we use them. Custom hierarchies for the domain. No bare
except. No silentpass. Wrap withraise NewError(...) from causeso debugging context survives the boundary. - Async is structured.
asyncio.TaskGroupfor parallel work,asyncio.timeoutfor bounds. Never bareasyncio.gatherwithout explicit error semantics. Neverasyncio.create_taskand drop the reference. - Small functions, breathing room. Hard limit: 50 lines. Aim 10–25. Python lacks braces and types in function bodies, so vertical density tends to be high — even short functions carry a lot of logic. Cap aggressively to force decomposition; lean on private helpers and comprehensions.
- Validate at every public boundary; assert internal invariants.
if not ...: raise ValueError(...)for input validation — this runs in production.assertfor internal invariants, never for security or contract checks (assertis stripped underpython -O). Aim for two checks per function on average. This density is a project discipline overlay, not native Pythonic practice — Python culture is "validate at the boundary, trust internally"; we tighten that to "validate at every public boundary, split compound checks, fail fast with a clear message." - Bound everything. All loops, retries, queues, timeouts, async tasks. No unbounded
while True. No recursion in library code where iteration works. Useitertools.isliceto bound lazy iterators when feeding them into APIs. - Use
match/casefor sealed sets; tag with a discriminator field. Exhaustivematchover a tagged union (sum types modeled withLiteraldiscriminators or sealed-class-style hierarchies) makes refactors visible — mypy flags missing cases. - Embrace Pythonic idioms — but deliberately. Context managers, generators, comprehensions, EAFP, decorators, dunders. Each has a right use. Reaching for the clever one when the boring one fits is anti-Zen.
Protocolover ABC. Composition over inheritance. AProtocoldocuments required behavior without forcing inheritance. An ABC forces a base class. Pick Protocol unless you genuinely need the base class for shared implementation.- Performance from the outset, but pay for what you use.
__slots__andfrozen=Trueon hot dataclasses. Generators for large/streaming data.functools.lru_cachefor pure-function memoization. Don't pre-optimize without a profile. - Zero technical debt. Public API is a contract — Python's lack of enforcement is not a license to break it.
__all__declares the surface. Semver is a promise.
This guide takes PEP 8 + PEP 20 as canonical, with PEP 484/604/695 governing types. The first entry below is a genuine softening — a place this guide deliberately relaxes the root canon to stay true to Python culture; the rest are additions the PEPs do not address (the function-size cap, and authorities layered on top of the base PEP set). Each is recorded so it can be revisited surgically.
| Rule | Upstream position | Our position | Why |
|---|---|---|---|
| Assertion density | Root canon: "assert aggressively," 2+ per function, no caveat | Same target, but reframed as a project discipline overlay — not native Pythonic practice | Python culture is "validate at the boundary, trust internally"; an aggressive-assertion mandate reads as un-Pythonic if presented as native. We keep the density but say plainly it is an overlay the language doesn't ask for: validate at every public boundary, split compound checks, fail fast. See rule 8 above and 08-error-handling.md. |
| Function size | No upstream cap (PEP 8 is silent) | 50-line hard cap, aim 10–25 | Owner decision; Tiger Style discipline, the tightest of the spine languages because Python bodies lack braces and type annotations, so vertical density runs high. See 05-functions.md and 01-formatting-and-tooling.md. |
| Added authorities | Root table names PEP 8 + PEP 20 + PEP 484/604 | Adds the Google Python Style Guide (docstrings, module structure) and PEP 695 (modern generics) on top | The base PEPs are silent on docstring shape and predate type-statement generics; the additions supply taste and modern syntax the PEPs leave open, and never override them. See 14-documentation.md and 03-type-hints.md. |
- PEP 8, PEP 20 — canonical Python.
- Google Python Style Guide — Google's adaptation; useful supplements on docstrings and module structure.
- Effective Python (Brett Slatkin) — community canon for idiomatic patterns.
- Hypermodern Python (Claudio Jolowicz) — modern tooling baseline (Ruff, mypy, pytest, pre-commit, pyproject.toml).
- Trio's structured concurrency — informs
asyncio.TaskGrouppatterns even though we use stdlib asyncio. - Azure SDK for Python Design Guidelines — prescriptive, battle-tested guidance on building Python SDKs: client class shape, constructor signature, method verb taxonomy (
get_*/list_*/create_*/begin_*), sync+async separation via.aiosubmodule, pageable iterators, long-running-operation pollers, conditional-request kwargs, retries/transport/credential injection. Adopted into chapters 02, 06, 08, 09, 10. - TigerBeetle Tiger Style — assertion density, 50-line function limit, limits on everything, no recursion, zero technical debt.
When adopting a new rule or migrating away from a deprecated pattern, apply the change at the module / package level or larger — never mix two styles within the same module. A half-migrated module is more confusing than either end state.
Perfection over technical debt — debt never gets paid