Skip to content

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.


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)

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.

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.

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.

Terminal window
cd backend && uv run ruff check .

To auto-fix:

Terminal window
cd backend && uv run ruff check . --fix

Ruff rule I001 (isort) enforces import ordering. Imports are grouped and sorted:

  1. Standard library
  2. Third-party
  3. Local application

Each group is alphabetically sorted and separated by a blank line.


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 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).

Terminal window
cd frontend && npx oxlint
cd frontend && npx tsc -b --noEmit

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
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 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:

  1. gitleaks — secret scan of the staged changes (needs the gitleaks binary on your PATH; CI pins the version in .forgejo/workflows/ci-security.yml and scans the full history)
  2. File hygiene — merge-conflict markers, case conflicts, valid YAML/TOML/JSON, private keys, files over 2 MB
  3. Commit message — Conventional Commits, and no AI or tool names or session trailers
  4. Generated and forbidden files — CHANGELOG.md is generated by git-cliff and not edited by hand; planning and agent directories are never committed
  5. ruff check and ruff check (cli) — Python linting
  6. APIError — raise HTTPException is rejected in backend/app/; errors go through APIError
  7. openapi.json — backend/openapi.json matches the app (export_openapi.py --check)
  8. uv.lock — both lock files match their pyproject.toml
  9. Alembic — the migrations have a single head
  10. oxlint and tsc — TypeScript/React linting and type checking
  11. Android design tokens — raw dp, hard-coded colors and Bold may only go down (android/scripts/check-design-tokens.sh)
  12. Documentation locale parity — every page has its fr/es/de/it translation with a current sourceHash
  13. Shell syntax — bash -n on every shell script
  14. pytest and pytest (cli) — test suites (on push only)
  15. 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.


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.