The base is PEP 8 and the Google Python Style Guide. Ruff enforces most of it and fixes what it can on save and in pre-commit. Everything in a "How it's enforced" line fails CI.
| Tool | Job |
|---|---|
uv |
Python version, dependencies, lockfile (uv.lock is committed) |
ruff format |
formatting. Not up for discussion in review. |
ruff check |
linting: style, bugs, naming, complexity, imports, docstrings |
pyright (strict) |
type checking |
pytest + coverage |
tests, with a minimum coverage threshold |
| SonarQube Cloud | duplication, code smells, maintainability rating on new code |
| Thing | Style | Example |
|---|---|---|
| module, package | snake_case, short |
links.py |
| function, method, variable | snake_case |
create_link |
| class, exception, type alias | PascalCase |
LinkRepository, LinkExpiredError |
| constant | UPPER_SNAKE_CASE, module level |
MAX_CODE_LENGTH |
| private | leading underscore | _hash_url |
| boolean | reads as a yes/no question | is_expired, has_owner |
- Names say what something is, not its type:
links, notlink_list. - No single letters except
i/jin short loops and_for ignored values. - No abbreviations unless they're more familiar than the full word (
url,id,db). - Functions are verbs (
delete_link), everything else is a noun.
How it's enforced: ruff N (pep8-naming) rules. The rest in review.
Each repo keeps a glossary in its AGENTS.md: one name for each domain concept,
used everywhere (variables, columns, API fields, docs). If the glossary says
target_url, then nothing is called long_url, destination or original.
Renaming a concept means renaming it everywhere in one PR.
How it's enforced: review, both human and AI. The AI reviewer is given the glossary.
-
Comments explain why: a constraint, a trade-off, a non-obvious bug being avoided. If a comment explains what the code does, rename things until it doesn't need to.
-
No commented-out code. Git remembers it.
-
TODOcomments link an issue:# TODO(#42): drop once clients are on v2. -
Every public module, class and function has a docstring in Google style:
def create_link(target_url: str, code: str | None = None) -> Link: """Store a new short link. Args: target_url: Where the short code redirects to. code: Custom short code. Generated if omitted. Returns: The stored link. Raises: CodeTakenError: If `code` is already in use. """
How it's enforced: ruff D (pydocstyle, Google convention), ERA (commented-out
code), TD (TODO format).
- Every function signature is fully typed. No implicit
Any. - Use built-in generics and
|(list[str],str | None), nottyping.ListorOptional. - Data crossing a boundary (API, DB, config) is a Pydantic model or a dataclass, not a dict.
How it's enforced: pyright strict, ruff ANN and UP.
- Logic that appears twice is a warning. Three times, extract it.
- Two pieces of code that look the same but change for different reasons stay separate. That's coincidence, not duplication.
- Magic numbers and strings become named constants.
How it's enforced: SonarQube Cloud quality gate (at most 3% duplicated lines in new
code), ruff PLR2004 (magic values), SIM (simplifiable code).
| Limit | Value | Rule |
|---|---|---|
| cyclomatic complexity per function | 10 | ruff C901 |
| arguments per function | 5 | ruff PLR0913 |
| branches per function | 12 | ruff PLR0912 |
| line length | 88 | ruff formatter |
- Raise specific exceptions defined in the repo (
LinkNotFoundError), not bareExceptionorValueErrorfor domain errors. - Never
except:orexcept Exception: pass. Catch what you can handle, let the rest propagate. - Translate domain errors to HTTP responses in one place (FastAPI exception handlers), not in every route.
How it's enforced: ruff BLE (blind except), E722, TRY, B.
Absolute imports, sorted by ruff (I): standard library, third party, local. No
wildcard imports.