21. Juni 2026

🔒 VERBINDLICH · v1.0✅ EXTERN GEPRÜFT

# BSN Coding Standards

Das verbindliche Regelwerk für sämtlichen Python-Code im Brettspiel-News-Community-Projekt. Basierend auf PEP 8, Google Python Style Guide und dem modernen Tooling-Konsens 2026. Extern reviewed und korrigiert.

📅 21. Juni 2026 🐍 Python 3.13+ 🔧 Ruff 0.15 + mypy 2.1 🇩🇪 Deutsche Docstrings

## 1 Allgemeine Prinzipien

#### „Readability counts." — Tim Peters, Zen of Python

Code wird 10× öfter gelesen als geschrieben. Jede Zeile muss für einen anderen Entwickler (oder dein zukünftiges Ich) sofort verständlich sein.

#### 📖 Lesbarkeit

Code ist Kommunikation. Unklares wird umgeschrieben — nicht mit Kommentar „erklärt".

#### 🎯 Explizit

Kein Magic. Typen, Parameter, Rückgabewerte — alles sichtbar.

#### 🧩 Einfach

YAGNI — keine Abstraktionen für hypothetische Zukunft.

#### 🛡️ Sichtbare Fehler

Exceptions nie still schlucken. Spezifisch fangen + loggen.

⚠️ Konsistenz-Regel (aus PEP 8):
Konsistenz mit diesem Styleguide ist wichtig. Konsistenz innerhalb des Projekts ist wichtiger. Konsistenz innerhalb eines Moduls ist am wichtigsten.
Regelbruch? Dokumentiere WARUM im Kommentar.

## 2 Code-Stil (PEP 8+)

Durchsetzung: `ruff check` + `ruff format` AUTO

### 2.1 Zeilenlänge

Maximal 100 Zeichen. 88 (Black-Default) ist zu knapp für Flask-Routen und SQLAlchemy-Queries.

### 2.2 Einrückung

- 4 Leerzeichen pro Ebene — NIE Tabs
- Fortsetzungszeilen: doppelte Einrückung (8) oder vertikale Ausrichtung

    # ✅ Richtig — vertikale Ausrichtung
    result = some_function(
        argument_one,
        argument_two,
        argument_three,
    )

    # ✅ Richtig — doppelte Einrückung bei Bedingungen
    if (some_very_long_condition
            and another_long_condition):
        do_something()

### 2.3 Quotes

- Doppelte Quotes (`"`) für Strings
- Einfache nur, wenn der String `"` enthält
- Docstrings: immer `"""triple double"""`

    def get_games() -> list[dict[str, object]]:
        """Holt alle Spiele aus der Datenbank."""
        ...

### 2.4 Imports

Reihenfolge (automatisch): 1. Standard-Library 2. Drittanbieter 3. Projekt-intern

🚫 Verboten: `from module import *` · Zirkuläre Imports

### 2.5 Trailing Commas

Immer bei mehrzeiligen Collections.

    items = ["Brettspiel", "Kartenspiel", "Würfelspiel",]

## 3 Typ-Annotationen

Durchsetzung: `mypy --strict` AUTO

#### Grundregel

Jede Funktion und Methode MUSS vollständige Typ-Annotationen haben. Keine Ausnahmen.

🔧 Review-Fix #1: Die ursprüngliche `Optional[str]`-Regel wurde umgedreht. Bei `requires-python = ">=3.13"` ist `X | None` (PEP 604) die idiomatische, empfohlene Form — und Ruff (UP045) schreibt `Optional[X]` automatisch zu `X | None` um. Regel und Linter sind jetzt im Einklang.

    # ✅ Richtig — X | None (PEP 604, idiomatisch für 3.13+)
    def get_game_by_id(game_id: int) -> dict[str, str | int] | None:
        """Holt ein Spiel anhand seiner ID."""
        ...

    # ❌ Falsch — keine Typen
    def get_game_by_id(game_id):
        ...

### 3.1 Spezifische Typ-Regeln

Regel | ✅ Richtig | ❌ Falsch
--- | --- | ---
Nullable mit `X | None` | `str | None` | `Optional[str]`
Built-in Generics (3.9+) | `list[dict[str, object]]` | `List[Dict]`
Generics parametrisieren | `dict[str, Any]` | `dict` (implizites `dict[Any, Any]`)
Explizites `-> None` | `def log(msg: str) -> None:` | `def log(msg: str):`

💡 Warum `X | None` und nicht `Optional[X]`?
Seit Python 3.10 / PEP 604 ist `X | None` der idiomatische Weg. `from typing import Optional` ist ein Relikt aus Python 3.9 und früher. Ruff (unter `UP045`) wandelt `Optional[X]` automatisch in `X | None` um — diesen Auto-Fix nutzen wir aktiv (`fixable = ["ALL"]`).

## 4 Namenskonventionen

Element | Konvention | Beispiel
--- | --- | ---
Module / Packages | `snake_case` | `game_service.py`
Klassen | `PascalCase` | `GameRepository`
Funktionen / Methoden | `snake_case` | `get_game_by_id()`
Variablen | `snake_case` | `game_count`
Konstanten | `UPPER_SNAKE_CASE` | `MAX_RESULTS = 50`
Private (intern) | `_leading_underscore` | `def _parse_response():`

### Zusätzlich

- Boolesche: `is_active`, `has_permission`, `should_retry`
- Funktionen als Verben: `get_`, `create_`, `validate_`
- Keine Ein-Buchstaben-Variablen (außer Comprehensions)
- Keine kryptischen Abkürzungen

## 5 Kommentare & Docstrings

#### 🇩🇪 Alle Kommentare und Docstrings auf DEUTSCH

Code-Schlüsselwörter sind Englisch. Alle menschenlesbaren Erklärungen: Deutsch.

### Google-Style Docstrings

    def berechne_durchschnitt(werte: list[float]) -> float:
        """Berechnet den arithmetischen Durchschnitt.

        Args:
            werte: Liste von Gleitkommazahlen.

        Returns:
            Der Durchschnittswert.

        Raises:
            ValueError: Wenn die Liste leer ist.
        """
        if not werte:
            raise ValueError("Die Werteliste darf nicht leer sein.")
        return sum(werte) / len(werte)

🚫 Auskommentierter Code: Nie! Dafür gibt's Git History.

## 6 Projektstruktur & Architektur

🔧 Review-Fix #4: Die „max 500 Zeilen / 50 Zeilen"-Grenzen sind jetzt teil-automatisiert: `C901` (McCabe-Komplexität) und `PLR0915` (too-many-statements, Default 50) wurden in Ruff aktiviert. Dateigrößen-Limits bleiben manuell — Ruff hat keinen „lines per file"-Checker. Die Tabelle unten zeigt klar, was automatisch und was manuell geprüft wird.

### 6.1 Maximalgrößen

Einheit | Maximum | Prüfung
--- | --- | ---
Modul (.py-Datei) | 500 Zeilen | MANUELL (Review)
Funktion / Methode | 50 Statements | AUTO (Ruff PLR0915)
Komplexität (McCabe) | max 10 | AUTO (Ruff C901)
Klasse | 300 Zeilen | MANUELL (Review)
Zeile | 100 Zeichen | AUTO (Ruff E501)

### 6.2 Verzeichnisstruktur

    project/
    ├── app.py              # Flask-App-Factory + Config
    ├── routes/             # NUR Request-Handling — keine Business-Logik
    ├── services/           # Business-Logik (Validierung, Berechnung, DB)
    ├── models/             # DB-Modelle — nur Schema, keine Logik
    ├── utils/              # Pure Functions — kein Zustand, keine DB
    ├── templates/          # Jinja2 — CSS IMMER inline
    ├── static/             # JS, Bilder
    ├── tests/              # Spiegelt src/-Struktur
    ├── migrations/         # Alembic
    ├── pyproject.toml      # ALLE Konfiguration an einem Ort
    ├── .pre-commit-config.yaml
    └── CODING_STANDARDS.md

### 6.3 Architekturprinzipien

#### 🚫 Keine God-Files

`app.py` ≠ 6000 Zeilen. Auslagern!

#### 🧠 Routes sind dumm

Request → Service → Response. Nichts weiter.

#### ⚙️ Services = Logik

Validierung, Berechnung, DB-Zugriffe.

#### 📦 Models = Schema

Tabellen-Definition. Keine Business-Logik.

## 7 Sicherheit

Durchsetzung: `ruff` (S-Regeln = flake8-bandit) AUTO

🔧 Review-Fix #2+3: Bandit als separater pre-commit-Hook wurde entfernt — Ruffs `S`-Regeln decken dieselben Checks ab, nur schneller, ohne separates Tool. Der B101-Skip wurde entfernt: asserts werden jetzt global geprüft, nur in `tests/**/*.py` per `per-file-ignores` erlaubt. Die assert-Regel aus 7.3 wird damit tatsächlich durchgesetzt.

### 7.1 Secrets

🚫 NIEMALS Secrets im Code! API-Keys, Passwörter, Tokens → immer Environment Variables. `.env` + `.db` in `.gitignore`.

### 7.2 Input-Validierung

- Alle User-Inputs validieren vor Verarbeitung
- SQL-Injection: Parameterisierte Queries (SQLAlchemy Default)
- XSS: User-Input escapen (Jinja2 Default)

### 7.3 Weitere Regeln

- Keine `assert`-Statements in Produktionscode AUTO
- Kein `pickle` für ungetrustete Daten
- `subprocess` mit `shell=False` (Default)
- Passwörter: `bcrypt` oder `argon2` — nie `md5`/`sha1`
- HTTPS überall — HSTS-Header setzen

## 8 Testing

Durchsetzung: `pytest --cov --cov-fail-under=80` CI (nicht in pre-commit)

🔧 Review-Fix (#6): TDD („erst Test, dann Code") ist jetzt eine Empfehlung, keine verbindliche Regel. Echte TDD-Pflicht wäre inkonsequent bei 80% Coverage-Floor. Der Floor bleibt hart bei 80%.

### 8.1 Grundregeln

1. 80% Code Coverage als harter Floor — unterschreiten blockiert den Merge in CI
2. Empfohlen: TDD (Test-Driven Development) — erst Test, dann Code
3. `pytest` als einziges Test-Framework
4. Tests in `tests/`, Struktur spiegelt `src/`

### 8.2 Test-Beispiel

    class TestGameService:
        def test_get_by_id_returns_game_when_found(self, db_session):
            # Arrange
            service = GameService(db_session)
            # Act
            result = service.get_by_id(1)
            # Assert
            assert result is not None
            assert result.name == "Catan"

## 9 Flask-spezifische Regeln

🔧 Review-Fix (#6): `def search_games() -> dict:` wurde zu `-> dict[str, object]` korrigiert. `dict` ohne Parameter ist `dict[Any, Any]` — das verstößt gegen mypy strict.

Regel | ✅ Richtig | ❌ Falsch
--- | --- | ---
`methods` explizit | `@app.route("/api/games", methods=["GET"])` | ohne methods
POST = `get_json()` | `data = request.get_json(silent=True)` ← None bei Parse-Fehler! | `request.args`
Response + Statuscode | `return data, 201` | `return data`
Fehler einheitlich | `{"error": "msg"}, 400` | plain string
Generics immer parametrisiert | `-> tuple[dict[str, object], int]` | `-> tuple[dict, int]`

    # ✅ Route — korrekte Typen, methods explizit
    @app.route("/api/search", methods=["GET"])
    def search_games() -> tuple[dict[str, object], int]:
        query = request.args.get("q", "").strip()
        if not query or len(query) > 100:
            return {"error": "Ungültiger Suchbegriff"}, 400
        return game_service.search(query), 200

### Config & UI

- `app.config.from_prefixed_env()` — Secrets nie hartkodieren
- CSS immer inline in Templates
- Mobile-first, Primärfarbe BSN-Blau #20228a
- Touch-Buttons min 64px, Overlays min 85vh

## 10 Git & Commits

### Conventional Commits

    <type>(<scope>): <description>

Typ | Verwendung
--- | ---
`feat` | Neues Feature
`fix` | Bugfix
`docs` | Dokumentation
`style` | Formatierung (kein Code-Change)
`refactor` | Code-Umbau
`test` | Tests
`chore` | Build, Deps, CI
`security` | Sicherheitsrelevante Änderung

### Commit-Hygiene

- Nach jeder abgeschlossenen Aufgabe committen
- Ein Commit = eine logische Änderung
- Vor jedem Commit: `pre-commit run` + `pytest`

## 11 Toolchain & Automatische Durchsetzung

🔧 Review-Fix #5: mypy wurde aus pre-commit entfernt. mypy im isolierten pre-commit-venv driftet garantiert vom echten Projekt-Environment weg (Stubs, Abhängigkeiten) und produziert andere Ergebnisse als `mypy .` im Projekt-venv. Stattdessen: mypy läuft lokal (Schritt 4 im Workflow) und in CI gegen das echte venv.
🔧 Review-Fix #3: Bandit wurde als separater Hook gestrichen. Ruffs `S`-Regeln (flake8-bandit) decken dieselben Sicherheits-Checks ab — ein Tool weniger, keine widersprüchlichen Findings.
🦀

#### Ruff 0.15

Linting + Formatierung
10-100× schneller als Flake8+Black

🔍

#### mypy 2.1

Statische Typ-Prüfung
⚠️ Major v2 (Mai 2026) — bei Problemen 1.20

🪝

#### pre-commit

Git-Hooks
Blockiert Commits bei Verstößen

🧪

#### pytest

Testing + Coverage
80% Coverage minimum

⚠️ mypy 2.1 — Breaking Changes (Mai 2026): Version 2.x ist sechs Wochen alt. Mit `strict = true` kann Code, der unter 1.x sauber lief, neue Fehler werfen (`--allow-redefinition`-Semantik, PEP 688 bytearray/memoryview). Bei Projektstart bewusst entscheiden: Entweder 2.1 nehmen (stricter) oder auf 1.20 bleiben (stabil) und nach dem ersten Milestone upgraden.

### 11.1 Täglicher Workflow

    # 1. Einmalig: Tools installieren
    pip install ruff mypy pre-commit pytest

    # 2. Einmalig: Hooks aktivieren
    pre-commit install

    # 3. Beim Coden: Formatieren + Linten (auto-fix!)
    ruff check . --fix
    ruff format .

    # 4. Typ-Prüfung (im Projekt-venv, NICHT in pre-commit)
    mypy .

    # 5. Tests ausführen
    pytest --cov --cov-fail-under=80

    # 6. Commit — pre-commit prüft AUTOMATISCH
    git commit -m "feat: neue Spiele-Suche"
    # → Ruff Lint + Format laufen automatisch
    # → Basic Checks (Merge-Konflikte, Whitespace, Debug-Statements)

### 11.2 Was wird automatisch geprüft?

Hook | Prüft | Blockiert bei
--- | --- | ---
Ruff (Lint) | Code-Stil, Fehler, Komplexität, Sicherheit | PEP-8-Verstößen, ungenutzten Imports, S-Regeln
Ruff (Format) | Einrückung, Quotes, Zeilenlänge | Abweichender Formatierung
Check-Merge-Conflict | Merge-Konflikte | `<<<<<<<` im Code
Trailing-Whitespace | Whitespace am Zeilenende | Leerzeichen/Tabs am Zeilenende
End-of-File-Fixer | Fehlende Newline am Dateiende | Dateien ohne abschließende Newline
Debug-Statements | `pdb`, `breakpoint()` | Debug-Code im Commit
Large Files | Dateigröße > 5 MB | Versehentliche Binaries

### 11.3 Was wird NICHT automatisch geprüft?

Diese Checks sind manuell im Review durchzuführen oder laufen in CI:

- mypy — läuft lokal + in CI gegen das echte venv (siehe Fix #5)
- Dateigrößen-Limits (500 Zeilen/Modul, 300 Zeilen/Klasse) — manuelles Review
- Coverage 80% — `pytest --cov` muss manuell ausgeführt werden (oder in CI)

### 11.4 CI-Setup (später)

    # .gitea/workflows/quality.yml (Gitea Actions — GitHub-kompatibel)
    - name: Lint + Security
      run: ruff check .
    - name: Type Check (im echten venv!)
      run: mypy .
    - name: Tests + Coverage
      run: pytest --cov --cov-fail-under=80

## 12 Code-Review-Checkliste

Bei jedem Code-Review durchgehen. AUTO = wird von pre-commit erzwungen.

- AUTO PEP 8 eingehalten? (Ruff-Check bestanden)
- AUTO Keine `from module import *`?
- AUTO Keine Debug-Statements (`pdb`, `breakpoint`)?
- AUTO Keine Merge-Konflikte im Code?
- Alle Funktionen haben vollständige Typ-Annotationen? (mypy bestanden)
- Docstrings auf Deutsch für alle öffentlichen Funktionen?
- Keine Secrets im Code?
- User-Input wird validiert?
- Tests geschrieben und grün? Coverage ≥ 80%?
- Keine God-Files (>500 Zeilen)? MANUELL
- Funktionen max 50 Statements, McCabe ≤ 10? AUTO
- Route enthält keine Business-Logik?
- Commit folgt Conventional Commits?
- Keine zirkulären Imports?

## 13 Konfigurationsdateien (Referenz)

Diese Dateien werden im Projekt-Root abgelegt. Alle Review-Fixes sind eingearbeitet.

### 13.1 pyproject.toml

    [build-system]
    requires = ["hatchling"]
    build-backend = "hatchling.build"

    [project]
    name = "bsn-community"
    version = "0.1.0"
    description = "Brettspiel-News Community-Projekt"
    requires-python = ">=3.13"
    readme = "README.md"
    license = {text = "Proprietary"}
    authors = [{name = "Daniel Krause", email = "info@brettspiel-news.de"}]

    # ── Ruff: Linting + Formatierung + Sicherheit ──
    [tool.ruff]
    line-length = 100
    indent-width = 4
    target-version = "py313"

    [tool.ruff.lint]
    select = [
        "E",     # pycodestyle errors
        "F",     # Pyflakes
        "W",     # pycodestyle warnings
        "I",     # isort (Import-Ordnung)
        "N",     # pep8-naming (Namenskonventionen)
        "UP",    # pyupgrade (auto-upgrade auf 3.13-Syntax)
        "B",     # flake8-bugbear (Common Bugs)
        "C4",    # flake8-comprehensions
        "SIM",   # flake8-simplify
        "TCH",   # flake8-type-checking
        "RUF",   # Ruff-spezifische Regeln
        "S",     # flake8-bandit — Sicherheit (ersetzt separaten Bandit-Hook!)
        "D",     # pydocstyle (Docstrings)
        "C90",   # McCabe-Komplexität
        "PL",    # Pylint-Regeln (too-many-*)
    ]
    ignore = [
        "D100",  # Modul-Docstring optional
        "D104",  # Package-Docstring optional
        "D107",  # __init__ Docstring optional
        "D203",  # Konflikt mit D211
        "D213",  # Konflikt mit D212
    ]
    fixable = ["ALL"]

    [tool.ruff.lint.mccabe]
    max-complexity = 10

    [tool.ruff.lint.pylint]
    max-statements = 50    # PLR0915 — entspricht „max 50 Statements/Funktion"
    max-args = 7           # PLR0913
    max-branches = 15      # PLR0912

    [tool.ruff.lint.per-file-ignores]
    "tests/**/*.py" = ["S101", "D103"]   # asserts + fehlende Docstrings in Tests erlaubt
    "migrations/**/*.py" = ["D"]         # Keine Docstring-Pflicht in Migrationen

    [tool.ruff.format]
    quote-style = "double"
    indent-style = "space"
    skip-magic-trailing-comma = false
    line-ending = "auto"

    # ── mypy: Statische Typ-Prüfung ──
    [tool.mypy]
    strict = true
    python_version = "3.13"
    exclude = ["migrations/", "tests/"]
    warn_unreachable = true
    warn_unused_ignores = true
    enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]

    # ── pytest: Testing ──
    [tool.pytest.ini_options]
    testpaths = ["tests"]
    python_files = "test_*.py"
    python_classes = "Test*"
    python_functions = "test_*"
    addopts = [
        "--strict-markers",
        "--tb=short",
        "--cov=src",
        "--cov-report=term-missing",
    ]
    markers = [
        "slow: Langsame Tests (Datenbank, API-Calls)",
        "integration: Integrationstests",
    ]

### 13.2 .pre-commit-config.yaml

    repos:
      - repo: https://github.com/astral-sh/ruff-pre-commit
        rev: v0.15.18
        hooks:
          - id: ruff
            name: "🔍 Ruff — Linting + Sicherheit"
            args: [--fix]
          - id: ruff-format
            name: "🎨 Ruff — Formatierung"

      # mypy läuft NICHT in pre-commit (isoliertes venv-Drift-Problem).
      # Stattdessen: manuell mit `mypy .` oder in CI gegen das echte venv.

      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v5.0.0
        hooks:
          - id: check-added-large-files
            name: "📦 Keine großen Dateien (>5 MB)"
            args: [--maxkb=5000]
          - id: check-merge-conflict
            name: "⚠️ Keine Merge-Konflikte"
          - id: trailing-whitespace
            name: "🧹 Kein Whitespace am Zeilenende"
          - id: end-of-file-fixer
            name: "📄 Dateien enden mit Newline"
          - id: check-yaml
            name: "📋 YAML-Syntax prüfen"
          - id: check-toml
            name: "📋 TOML-Syntax prüfen"
          - id: debug-statements
            name: "🐛 Keine Debug-Statements (pdb, breakpoint)"

## 14 Änderungshistorie (Review-Fixes)

Diese Version enthält alle Korrekturen aus dem externen Code-Review vom 21. Juni 2026.

# | Problem | Fix | Betroffene Sektionen
--- | --- | --- | ---
1 | Optional[X] vs X | None — Regel widersprach sich selbst und kämpfte gegen RUFF (UP045) | `X | None` als Standard (PEP 604, idiomatisch für 3.13). `Optional` aus allen Beispielen entfernt. Warnkasten durch Info-Box mit Begründung ersetzt. | §3, alle Code-Beispiele
2 | B101 global deaktiviert — „kein assert in Produktion" wurde nie durchgesetzt | Globalen B101-Skip entfernt. Asserts nur in `tests/**/*.py` per `per-file-ignores` erlaubt. | §7, §13 (pyproject.toml)
3 | Bandit doppelt — separater Hook + Ruffs S-Regeln = Redundanz | Bandit-Hook komplett gestrichen. Ruffs `S`-Regeln decken alles ab. | §11, §13 (.pre-commit-config.yaml)
4 | God-File/Function als „automatisch" behauptet, aber keine Regeln konfiguriert | `C90` + `PL` zu Ruff-Select hinzugefügt. McCabe max 10, Statements max 50. Dateigrößen-Limits klar als MANUELL markiert. | §6, §12, §13
5 | mypy in pre-commit — isoliertes venv driftet vom Projekt-Environment | mypy aus .pre-commit-config.yaml entfernt. Läuft stattdessen lokal (`mypy .`) und in CI. | §11, §13
6 | Kleinkram: TDD-Pflicht inkonsequent, `-> dict:` verstößt gegen mypy strict, Versions-Pins veraltet | TDD → Empfehlung. `dict` → `dict[str, object]`. Ruff 0.11.3 → 0.15.18, mypy 1.15.0 → 2.1.0. | §8, §9, §13
Zweite Review-Runde — 21. Juni 2026
7 | UP007 vs UP045 — falsche Regel-Nummer in der Info-Box | `UP007` → `UP045` korrigiert. Seit Ruff 0.12: UP007 = typing.Union, UP045 = typing.Optional. | §3, §14
8 | Coverage als AUTO gelabelt — pre-commit führt kein pytest aus | „blockiert den Commit" → „blockiert den Merge in CI". Coverage-Label ehrlich als CI-Check (nicht pre-commit). | §8
9 | mypy 2.1 Bleeding-Edge — Major v2 (Mai 2026) mit Breaking Changes | Warnbox in §11 ergänzt. Empfehlung: Entweder 2.1 bewusst nehmen, oder auf 1.20 bleiben. | §11
10 | CI-Pfad GitHub-flavored und `silent=True` None-Falle | `.github/` → `.gitea/`. `get_json(silent=True)` mit None-Hinweis versehen. | §9, §11