Code Style & Linting
This document defines the code style and linting conventions for the Patrimoine project. The project enforces these rules through automated tooling — code that fails linting will not pass CI.
Python (Backend)
Section titled “Python (Backend)”Ruff Configuration
Section titled “Ruff Configuration”Ruff is the sole Python linter and formatter. Configuration lives in backend/pyproject.toml.
| Setting | Value |
|---|---|
| Linter rules | E, F, I, UP, B, SIM, C901 |
| Line length | 100 |
| Target version | py313 |
| Ignored rules | B008 in app/** (FastAPI Depends() calls in function defaults); C901 in alembic/versions/** (migrations are historical and immutable) |
Rule breakdown:
- E — pycodestyle errors (formatting, whitespace)
- F — pyflakes (unused imports, undefined names)
- I — isort (import ordering, rule
I001) - UP — pyupgrade (modern Python syntax)
- B — flake8-bugbear (common pitfalls)
- SIM — flake8-simplify (simplifiable code patterns)
- C901 — mccabe cyclomatic complexity (see “Complexity” below)
Complexity
Section titled “Complexity”Ruff enforces mccabe cyclomatic complexity (C901) at max-complexity = 15
([tool.ruff.lint.mccabe] in backend/pyproject.toml). A function scoring above 15 branches
enough that a reviewer can no longer hold every path in their head at once — that is the
maintainability risk this rule targets, not line count.
Why C901 and not the pylint size rules (PLR0912/PLR0915)
Section titled “Why C901 and not the pylint size rules (PLR0912/PLR0915)”Those rules flag functions by
branch/statement count, which also catches long-but-linear code: flat orchestrators (the
_restore_*(ctx) / _export_*(ctx) dispatch sequences in services/backup_import//export_service.py),
declarative section functions, and Alembic migrations.
None of those are hard to review — they’re
long because they enumerate cases, not because they branch unpredictably. C901 measures actual
branching (independent paths through the function), which is what makes a function genuinely hard
to reason about or test.
Enabling PLR0912/PLR0915 alongside C901 would add noise on exactly
the code that was already flattened, without catching anything C901 misses. Decision:
not enabled.
Grandfathered exceptions
Section titled “Grandfathered exceptions”A handful of functions predate this rule and still score above 15.
Each carries an explicit # noqa: C901 on its def line (or on the line immediately above, when
adding the reason inline would push the line past 100 columns), with a short reason:
| Function | File | Score |
|---|---|---|
lifespan |
app/main.py |
58 |
build_preview |
app/services/import_service.py |
23 |
commit_csv_import |
app/services/csv_category_import_service.py |
21 |
_restore_accounts |
app/services/backup_import/accounts.py |
19 |
_parse_backup_transaction_row |
app/services/backup_import/support.py |
19 |
update_user_profile |
app/services/auth_service.py |
18 |
apply_transaction_filters |
app/services/transaction_service.py |
17 |
parse_bank_csv |
app/services/spreadsheet_parsing.py |
17 |
These are not refactored as part of adding the rule — that’s separate work, tracked outside this policy. The rule’s job is to stop the next god-function, not retroactively fix every existing one; the table above is the explicit backlog.
Policy going forward
Section titled “Policy going forward”A new function scoring above 15 must be split, not given a noqa.
The only exception is when a reviewer explicitly agrees an exception is warranted for that specific
function (the same “branches per declarative case, not per decision” shape as the grandfathered
list above) — and even then, the reason must be written on the noqa line, not just agreed upon
in review and left undocumented in code.
Running Ruff
Section titled “Running Ruff”cd backend && uv run ruff check .To auto-fix:
cd backend && uv run ruff check . --fixImport Ordering (Python)
Section titled “Import Ordering (Python)”Ruff rule I001 (isort) enforces import ordering. Imports are grouped and sorted:
- Standard library
- Third-party
- Local application
Each group is alphabetically sorted and separated by a blank line.
TypeScript (Frontend)
Section titled “TypeScript (Frontend)”TypeScript Compiler
Section titled “TypeScript Compiler”Strict mode is enabled with additional safety flags. Configuration lives in frontend/tsconfig.app.json.
| Flag | Value |
|---|---|
strict |
true |
noUnusedLocals |
true |
noUnusedParameters |
true |
erasableSyntaxOnly |
true |
verbatimModuleSyntax |
true |
OxLint
Section titled “OxLint”OxLint is the linter for TypeScript/React code. Configuration lives in frontend/.oxlintrc.json.
Enabled plugins:
- react — React-specific rules (hooks, JSX best practices)
- typescript — TypeScript-specific rules
- oxc — oxlint’s own correctness rules
A no-restricted-imports rule forbids importing toast from sonner (use @/lib/toast).
Running Linters
Section titled “Running Linters”cd frontend && npx oxlintcd frontend && npx tsc -b --noEmitNaming Conventions
Section titled “Naming Conventions”Python
Section titled “Python”| Element | Convention | Example |
|---|---|---|
| Variables, functions, methods | snake_case |
get_account_by_id |
| Classes | PascalCase |
AccountRepository |
| Constants | UPPER_SNAKE_CASE |
MAX_RETRIES |
| Modules/files | snake_case |
account_service.py |
| Pydantic schemas | PascalCase + suffix |
AccountCreate, AccountOut |
TypeScript
Section titled “TypeScript”| Element | Convention | Example |
|---|---|---|
| Variables, functions | camelCase |
formatCurrency |
| Components, types, interfaces | PascalCase |
AccountCard, SnapshotData |
| Constants | UPPER_SNAKE_CASE |
API_BASE_URL |
| Files (general) | kebab-case |
format-currency.ts |
| React component files | PascalCase.tsx |
AccountCard.tsx |
Pre-commit Hooks
Section titled “Pre-commit Hooks”Pre-commit hooks run automatically via prek from prek.toml; run prek install once to install the commit-msg, pre-commit and pre-push hooks. Every check except pytest runs on each commit (the commit message checks on the message); pytest and the signature check run on git push. Skip one hook for a single commit with SKIP=<hook-id> git commit, never the signature.
The following checks are executed:
- gitleaks — secret scan of the staged changes (needs the
gitleaksbinary on yourPATH; CI pins the version in.forgejo/workflows/ci-security.ymland scans the full history) - File hygiene — merge-conflict markers, case conflicts, valid YAML/TOML/JSON, private keys, files over 2 MB
- Commit message — Conventional Commits, and no AI or tool names or session trailers
- Generated and forbidden files —
CHANGELOG.mdis generated by git-cliff and not edited by hand; planning and agent directories are never committed - ruff check and ruff check (cli) — Python linting
- APIError —
raise HTTPExceptionis rejected inbackend/app/; errors go throughAPIError - openapi.json —
backend/openapi.jsonmatches the app (export_openapi.py --check) - uv.lock — both lock files match their
pyproject.toml - Alembic — the migrations have a single head
- oxlint and tsc — TypeScript/React linting and type checking
- Android design tokens — raw
dp, hard-coded colors andBoldmay only go down (android/scripts/check-design-tokens.sh) - Documentation locale parity — every page has its fr/es/de/it translation with a current
sourceHash - Shell syntax —
bash -non every shell script - pytest and pytest (cli) — test suites (on push only)
- Signed commits — every commit being pushed carries a valid signature (on push only)
If any hook fails, the commit (or the push, for pytest) is blocked. Fix the issues and re-stage.
Comments
Section titled “Comments”No comments in code unless explicitly requested.
Code should be self-documenting through clear naming and structure. If a comment is absolutely necessary (e.g., explaining a non-obvious algorithm or a workaround), keep it brief and focused on why, not what.