Formatting is not a matter of taste. We delegate to tools and disagree elsewhere.
"""Account balances, formatted and tooled to the house standard."""
from dataclasses import dataclass
from decimal import Decimal
__all__ = ["Account", "active_balances"]
@dataclass(frozen=True)
class Account:
owner: str
balance: Decimal
currency: str = "USD"
def active_balances(accounts: list[Account]) -> dict[str, Decimal]:
"""Map each non-empty owner to their balance, dropping zeroed accounts."""
return {
account.owner: account.balance
for account in accounts
if account.balance > 0
}This module imports explicitly in sorted, grouped order (1.6) and declares its public surface in __all__ just below those imports (1.5). Every line stays inside 100 columns (1.2); the dict comprehension reads cleaner than a for/append loop and earns its place by staying under two lines of logic with a single if (1.7). The frozen dataclass orders its non-defaulted fields before the defaulted currency (review territory for 1.1). The comparison balance > 0 is explicit rather than a truthy check (1.8), and the function clears the 50-line cap with room to spare (1.4).
Reasoning, step by step:
- Argument over whitespace is a tax on every PR. A deterministic formatter ends the discussion.
- Ruff (modern, fast, written in Rust) replaces Black + isort + flake8 + pyupgrade + pylint-subset in one tool. It's the new default for serious Python projects.
- mypy (or pyright) checks types — Python's optional, gradual type system is only as good as the checker that enforces it.
- All three run in CI on every PR. They also run pre-commit on every developer machine.
- Format-disable comments (
# fmt: off,# noqa,# type: ignore) need a TODO with an owner and a date. Tracked.
Minimum pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF", "ANN", "ASYNC", "S"]
ignore = ["ANN101", "ANN102"] # self/cls type hints
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = trueEnforcement: CI job running ruff format --check, ruff check, and mypy --strict; pre-commit mirrors all three locally (1.10).
Reasoning, step by step:
- PEP 8 says 79; Black's default is 88; Google says 80. We pick 100 because typed Python signatures, dataclass fields, and Protocol method signatures genuinely run wider than untyped Python.
- 100 fits side-by-side diffs at modern resolutions. Wider lines break review.
- Wrap at semantic boundaries — between arguments, after
=, between chained method calls — not mid-expression. - If a line still wraps after argument-wrapping, the function is doing too much.
Enforcement: Ruff line-length = 100 (E501) under [tool.ruff].
Reasoning, step by step:
- Trailing commas reduce diff noise — adding an element touches one line, not two.
- They make argument-order swaps a one-line change.
- Ruff enforces this; configure it.
- Required especially in
__all__, function calls with named args, and tuple literals where the comma is sometimes semantically required ((item,)for single-element tuple).
Enforcement: ruff format, which adds magic trailing commas, plus the COM lint rules.
Reasoning, step by step:
- A function you can't see at once on one screen costs you context every time you read it.
- Python is more concise per line than Kotlin or Go (no braces, no semicolons, no types in body), so we go tighter than Kotlin's 60.
- Counting: signature, blank lines, and closing — all count. Docstrings do not.
- Approaching 50? Extract a private helper, lift a comprehension out, or split into a class with smaller methods.
- The cap is a signal, not a target. Don't compress for the number.
Enforcement: Ruff PLR0915 (too-many-statements) tuned to the cap; review for the 10–25 aim.
Reasoning, step by step:
- Python has no real visibility modifier.
__all__is the convention: a list of names thatfrom module import *will expose and that documents your public contract. - Without
__all__, every top-level name is "public" by default — refactoring is then a guessing game about what callers depend on. - Place
__all__at the top of the file, just below imports:__all__ = ["UserId", "load_user", "User"]
- Module-level helpers not in
__all__are conventionally_private(leading underscore).
Enforcement: Ruff RUF022 (unsorted __all__) and F822 (undefined name in __all__); review for presence.
Reasoning, step by step:
- Ruff (with
isortrules) sorts imports into three groups: stdlib, third-party, local. Within each group, alphabetical. from foo import *is banned outside__init__.pyre-exports and unless the module deliberately defines__all__.- Relative imports (
from .utils import x) only within a package, only when they make the dependency clearer than absolute. In application code, prefer absolute. - Conditional imports (inside functions) for genuinely circular or optional-dependency cases only. Document why.
Enforcement: Ruff I (isort) for order and grouping, F403/F405 for star imports.
Reasoning, step by step:
[x.id for x in items if x.active]is clearer than aforloop withappend. Use the comprehension.- The comprehension stops being clearer when (a) it spans more than 2 lines, (b) it has more than one
if, (c) the inner expression is non-trivial. - At that point, write a
forloop. Readability wins over Python golf. - Ternary
a if cond else bis fine for one-liners. Nested ternaries are not — write anif/elif/else.
Enforcement: Ruff SIM (C419, nested-ternary) flags over-compression; review for the 2-line/one-if ceiling.
1.8 — Conditionals: no parentheses around the condition; no truthy-checks on None / empty sequences when explicit is clearer.
Reasoning, step by step:
if user is None:is unambiguous.if not user:is true forNone, empty string,0, empty list — usually not what you meant.if items:is fine for "the list is non-empty."if items is not None:is what you need ifNoneis a different signal than empty.- PEP 8: explicit comparisons against
Noneuseis/is not, never==/!=.
Enforcement: Ruff E711/E712 (comparison to None/True) and SIM truthiness rules; review for intended-empty vs. None.
Reasoning, step by step:
- Two blank lines between top-level functions and classes.
- One blank line between methods.
- One blank line inside a function to separate logical sections (rare — if you need many, split the function).
- No trailing whitespace.
- Final newline at EOF.
Enforcement: ruff format (E303 blank-line rules, W291 trailing whitespace, W292 final newline).
Reasoning, step by step:
pre-commit(https://pre-commit.com/) catches formatting and type errors before they reach the remote.- Minimum
.pre-commit-config.yaml:repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.x.x hooks: - id: ruff-format - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.x.x hooks: - id: mypy
- Pin tool versions. Don't let
latestratchet your project quietly.
Enforcement: committed .pre-commit-config.yaml with pinned revs; CI runs pre-commit run --all-files.
Reasoning, step by step:
pyproject.toml(PEP 518, PEP 621) holds: build system, project metadata, dependencies, and tool config (Ruff, mypy, pytest).- Don't scatter
setup.cfg,tox.ini,.flake8,.mypy.ini. Consolidate. - Exception:
pre-commitand CI workflow files live where their tools expect them.
Enforcement: review; no setup.cfg/tox.ini/.flake8/.mypy.ini in the tree alongside pyproject.toml.
Reasoning, step by step:
- If the formatter and the codebase disagree, one of them is wrong. Fix the rule or fix the code.
- Pin versions. Don't let
latestratchet quietly. - Format-on-save in IDE; format-on-commit in pre-commit; format-check in CI. Three rings of defense.
Enforcement: pinned tool versions in pyproject.toml/.pre-commit-config.yaml; CI fails on any --check drift.
- Type hints in detail: chapter 03.
- Module organization and
__init__.py: chapter 12. - Docstring style: chapter 14.