# BSN Coding Standards — Das verbindliche Regelwerk > **Version:** 1.0 — 21. Juni 2026 > **Autor:** Cody (Coding-Agent für Daniel Krause, brettspiel-news.de) > **Verbindlichkeit:** Diese Regeln werden durch automatisierte Tools (pre-commit, Ruff, mypy) durchgesetzt. Kein Merge ohne bestandene Checks. --- ## Inhaltsverzeichnis 1. [Allgemeine Prinzipien](#1-allgemeine-prinzipien) 2. [Code-Stil](#2-code-stil) 3. [Typ-Annotationen](#3-typ-annotationen) 4. [Namenskonventionen](#4-namenskonventionen) 5. [Kommentare & Docstrings](#5-kommentare--docstrings) 6. [Projektstruktur & Architektur](#6-projektstruktur--architektur) 7. [Sicherheit](#7-sicherheit) 8. [Testing](#8-testing) 9. [Flask-spezifische Regeln](#9-flask-spezifische-regeln) 10. [Git & Commits](#10-git--commits) 11. [Toolchain & Durchsetzung](#11-toolchain--durchsetzung) --- ## 1. Allgemeine Prinzipien ### 1.1 Das Zen of Python (PEP 20) ```python import this ``` Die wichtigsten Prinzipien für dieses Projekt: | Prinzip | Bedeutung | |---|---| | **Readability counts.** | Code wird 10× öfter gelesen als geschrieben. Schreibe für den Leser. | | **Explicit is better than implicit.** | Kein Magic. Kein `**kwargs`-Missbrauch. Typen immer sichtbar. | | **Simple is better than complex.** | YAGNI — You Ain't Gonna Need It. Keine überflüssigen Abstraktionen. | | **Errors should never pass silently.** | Immer `try/except` mit spezifischen Exceptions + Logging. | | **There should be one obvious way to do it.** | Einheitlicher Stil im gesamten Projekt. | ### 1.2 Konsistenz-Regel (PEP 8) > **Konsistenz mit diesem Styleguide ist wichtig. Konsistenz innerhalb des Projekts ist wichtiger. Konsistenz innerhalb eines Moduls ist am wichtigsten.** Wenn du eine Regel brechen musst, dokumentiere WARUM in einem Kommentar. --- ## 2. Code-Stil **Durchsetzung:** Ruff (Linter + Formatter) in `pyproject.toml` **Basis:** PEP 8 mit projektspezifischen Anpassungen ### 2.1 Zeilenlänge ``` Maximal 100 Zeichen pro Zeile. ``` **Begründung:** 88 (Black-Default) ist zu knapp für Flask-Routen und SQLAlchemy-Queries. 100 Zeichen erlauben lesbare Code-Blöcke ohne horizontales Scrollen. ### 2.2 Einrückung - **4 Leerzeichen** pro Ebene — **NIE Tabs** - Fortsetzungszeilen: 8 Leerzeichen (doppelte Einrückung) oder vertikale Ausrichtung ```python # ✅ Richtig — vertikale Ausrichtung result = some_function( argument_one, argument_two, argument_three, ) # ✅ Richtig — doppelte Einrückung bei langen Bedingungen if (some_very_long_condition and another_long_condition): do_something() # ❌ Falsch — nur 4 Leerzeichen (nicht unterscheidbar) if (some_very_long_condition and another_long_condition): do_something() ``` ### 2.3 Quotes - **Doppelte Anführungszeichen** (`"`) für Strings — Black/Ruff-Standard - Einfache Anführungszeichen nur, wenn der String selbst `"` enthält - Docstrings: **immer** `"""triple double quotes"""` (PEP 257) ```python # ✅ Richtig name = "Brettspiel-News" message = f"Willkommen bei {name}!" # ✅ Richtig — enthält doppelte Quotes quote = 'Er sagte: "Das ist ein gutes Spiel!"' # ✅ Richtig def get_games() -> list[dict]: """Holt alle Spiele aus der Datenbank.""" ... ``` ### 2.4 Imports **Reihenfolge** (automatisch durch Ruff/isort): 1. Standard-Library (`os`, `json`, `datetime`) 2. Drittanbieter (`flask`, `sqlalchemy`, `requests`) 3. Projekt-interne (`from .models import ...`) ```python # ✅ Richtig import os import json from datetime import datetime, timedelta from flask import Flask, request, jsonify from sqlalchemy import create_engine from .models import Game, User from .utils import validate_email ``` **Verboten:** - `from module import *` — verschmutzt den Namespace - Zirkuläre Imports — deuten auf Architekturprobleme hin ### 2.5 Leerzeichen | Regel | Beispiel | |---|---| | **Nach Komma** ein Leerzeichen | `[1, 2, 3]` nicht `[1,2,3]` | | **Um Operatoren** ein Leerzeichen | `x = 1 + 2` nicht `x=1+2` | | **Kein Leerzeichen** vor `(` bei Funktion | `def foo():` nicht `def foo ():` | | **Kein Leerzeichen** vor `:` bei Slice | `list[1:3]` nicht `list[1 : 3]` | | **Kein Leerzeichen** vor `=` bei Keyword-Arg | `foo(x=1)` nicht `foo(x = 1)` | ### 2.6 Leerzeilen - 2 Leerzeilen vor Top-Level-Funktionen und -Klassen - 1 Leerzeile vor Methoden innerhalb einer Klasse - 1 Leerzeile zwischen logischen Blöcken innerhalb einer Funktion ### 2.7 Trailing Commas - **Immer** trailing comma bei mehrzeiligen Listen/Dicts/Tupeln - Erleichtert Diffs und verhindert Fehler beim Hinzufügen von Elementen ```python # ✅ Richtig items = [ "Brettspiel", "Kartenspiel", "Würfelspiel", ] config = { "host": "localhost", "port": 5432, "debug": False, } ``` --- ## 3. Typ-Annotationen **Durchsetzung:** mypy in `strict`-Mode ### 3.1 Grundregel **Jede Funktion und Methode MUSS vollständige Typ-Annotationen haben.** ```python # ✅ Richtig 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.2 Spezifische Regeln | Regel | Richtig | Falsch | |---|---|---| | `X \| None` (PEP 604, idiomatisch für 3.13+) | `str \| None` | `Optional[str]` | | `list`/`dict` statt `List`/`Dict` (Python 3.9+) | `list[str]` | `List[str]` | | Generics immer parametrisieren | `dict[str, Any]` | `dict` (implizites `dict[Any, Any]`) | | `-> None` bei Funktionen ohne Return | `def log(msg: str) -> None:` | `def log(msg: str):` | ```python # ✅ Richtig def find_user(email: str) -> dict[str, object] | None: """Sucht einen User per E-Mail. Gibt None zurück, wenn nicht gefunden.""" ... # ✅ Richtig — Flask-Route @app.route("/api/games/") def get_game(game_id: int) -> tuple[dict[str, object], int]: """Gibt ein einzelnes Spiel zurück.""" ... ``` --- ## 4. Namenskonventionen ### 4.1 Übersicht | Element | Konvention | Beispiel | |---|---|---| | **Module/Packages** | `snake_case` | `game_service.py`, `user_models.py` | | **Klassen** | `PascalCase` | `GameRepository`, `UserService` | | **Funktionen/Methoden** | `snake_case` | `get_game_by_id()`, `calculate_score()` | | **Variablen** | `snake_case` | `game_count`, `user_email` | | **Konstanten** | `UPPER_SNAKE_CASE` | `MAX_RESULTS = 50`, `DEFAULT_TIMEOUT = 30` | | **Private (intern)** | `_leading_underscore` | `def _parse_response():`, `self._cache` | | **"Private" (Name Mangling)** | `__double_underscore` | `self.__db_connection` (nur bei Vererbungskonflikten) | ### 4.2 Spezifische Regeln - **Boolesche Variablen** mit `is_`, `has_`, `should_`-Präfix: `is_active`, `has_permission`, `should_retry` - **Funktionen** als Verben: `get_`, `create_`, `update_`, `delete_`, `calculate_`, `validate_` - **Keine Ein-Buchstaben-Variablen** außer in trivialen Comprehensions (`[x for x in items]`) - **Keine kryptischen Abkürzungen**: `user_count` statt `uc`, `response` statt `resp` --- ## 5. Kommentare & Docstrings ### 5.1 Sprache **Alle Kommentare und Docstrings werden auf DEUTSCH geschrieben.** ```python # ✅ Richtig def berechne_durchschnitt(werteliste: list[float]) -> float: """Berechnet den arithmetischen Durchschnitt einer Werteliste. Args: werteliste: Liste von Gleitkommazahlen. Returns: Der Durchschnittswert. Raises: ValueError: Wenn die Liste leer ist. """ if not werteliste: raise ValueError("Die Werteliste darf nicht leer sein.") return sum(werteliste) / len(werteliste) ``` ### 5.2 Docstring-Format **Google-Style Docstrings** (maschinenlesbar, von den meisten Tools unterstützt): ```python def komplexe_funktion( name: str, werte: list[int], optional: str | None = None, ) -> dict[str, object]: """Kurzbeschreibung (eine Zeile). Ausführliche Beschreibung. Kann mehrere Sätze umfassen. Beschreibt das WARUM, nicht das WAS. Args: name: Beschreibung des Parameters. werte: Liste von Werten, die verarbeitet werden. optional: Optionaler Zusatzparameter. Returns: Ein Dictionary mit den verarbeiteten Ergebnissen. Raises: ValueError: Wenn name leer ist. TypeError: Wenn werte keine Liste von ints ist. Example: >>> komplexe_funktion("Test", [1, 2, 3]) {"name": "Test", "summe": 6} """ ``` ### 5.3 Wann kommentieren? - **Immer:** Docstrings für alle öffentlichen Funktionen/Klassen - **Oft:** `# TODO:`, `# FIXME:`, `# HACK:` mit Erklärung - **Manchmal:** Erklärung von nicht-offensichtlichem Code (Algorithmus, Workaround) - **Nie:** Code auskommentieren — bei Bedarf durch Git-History wiederherstellen ```python # ✅ Richtig — erklärt WARUM der Workaround # Workaround: SQLAlchemy 2.0 deprecated `query` — bis Migration nutzen wir `session.execute()` result = session.execute(select(Game).where(Game.id == game_id)) # ❌ Falsch — erklärt WAS (offensichtlich) # Holt das Spiel mit der ID result = session.execute(select(Game).where(Game.id == game_id)) ``` --- ## 6. Projektstruktur & Architektur ### 6.1 Maximalgrößen | Einheit | Maximal | |---|---| | **Modul (.py-Datei)** | 500 Zeilen | | **Funktion/Methode** | 50 Zeilen | | **Klasse** | 300 Zeilen | | **Zeile** | 100 Zeichen | **Begründung:** Kleine, fokussierte Einheiten sind leichter zu testen, zu verstehen und zu warten. ### 6.2 Separation of Concerns ``` project/ ├── app.py # Flask-App-Factory + Config ├── routes/ # Nur Request-Handling, keine Business-Logik │ ├── __init__.py │ ├── games.py # /api/games/* │ └── users.py # /api/users/* ├── services/ # Business-Logik │ ├── __init__.py │ └── game_service.py ├── models/ # Datenbank-Modelle │ ├── __init__.py │ └── game.py ├── utils/ # Hilfsfunktionen │ ├── __init__.py │ └── validators.py ├── templates/ # Jinja2-Templates ├── static/ # CSS, JS, Bilder ├── tests/ # Tests (spiegelt src/-Struktur) │ ├── test_routes/ │ ├── test_services/ │ └── conftest.py ├── migrations/ # DB-Migrationen (Alembic) ├── pyproject.toml # ALLE Projekt-Konfiguration ├── .pre-commit-config.yaml ├── README.md └── CODING_STANDARDS.md ``` ### 6.3 Prinzipien - **Keine God-Files:** `app.py` darf nicht 6000 Zeilen haben. Auslagern! - **Routes sind dumm:** Nur Request-Parsing → Service aufrufen → Response formatieren - **Services enthalten Logik:** Validierung, Berechnung, DB-Zugriffe - **Models nur Schema:** Tabellen-Definition, keine Business-Logik - **Utils sind pure functions:** Kein Zustand, keine Seiteneffekte, keine DB-Zugriffe ```python # ✅ Richtig — Route delegiert an Service @app.route("/api/games/") def get_game(game_id: int) -> tuple[dict, int]: """Gibt ein einzelnes Spiel zurück.""" game = game_service.get_by_id(game_id) if not game: return {"error": "Spiel nicht gefunden"}, 404 return game.to_dict(), 200 # ❌ Falsch — Route enthält Business-Logik @app.route("/api/games/") def get_game(game_id): game = db.session.execute( select(Game).where(Game.id == game_id) ).scalar_one_or_none() if not game: return {"error": "Not found"}, 404 # 50 Zeilen Berechnungen hier... ``` --- ## 7. Sicherheit **Durchsetzung:** Ruff (Sicherheits-Regeln) + Bandit ### 7.1 Secrets **NIEMALS Secrets im Code!** Immer über Environment Variables oder `.env`-Datei. ```python # ✅ Richtig import os API_KEY = os.environ["BSN_API_KEY"] # ❌ Falsch API_KEY = "sk-abc123def456" # SOFORT LÖSCHEN + Key rotieren! ``` **`.env` und `.db`-Dateien MÜSSEN in `.gitignore` stehen.** ### 7.2 Input-Validierung - **Alle User-Inputs validieren**, bevor sie verarbeitet werden - Flask-Werkzeuge nutzen: `request.args.get()`, `request.json.get()` mit Defaults - SQL-Injection verhindern: Parameterisierte Queries (SQLAlchemy macht das automatisch) ```python # ✅ Richtig — validiert und sanitized from flask import request, abort @app.route("/api/search") def search_games() -> dict: query = request.args.get("q", "").strip() if not query or len(query) > 100: abort(400, description="Ungültiger Suchbegriff") return game_service.search(query) # ❌ Falsch — keine Validierung, roher Input in Query @app.route("/api/search") def search_games(): query = request.args.get("q") db.session.execute(f"SELECT * FROM games WHERE name LIKE '%{query}%'") ``` ### 7.3 Weitere Regeln - **Keine assert-Statements** in Produktionscode (werden mit `-O` deaktiviert) - **Keine `pickle`** für ungetrustete Daten - **`subprocess` mit `shell=False`** (Default) - **Hash-Funktionen für Passwörter:** `bcrypt` oder `argon2`, nie `md5`/`sha1` --- ## 8. Testing **Durchsetzung:** pytest mit Coverage-Check ### 8.1 Grundregeln 1. **Jede neue Funktion → Test** (Empfehlung: TDD — erst Test, dann Code) 2. **80% Code Coverage** als harter Floor — unterschreiten blockiert den Commit 3. **pytest** als einziges Test-Framework 4. **Tests in `tests/`**, spiegelt die Projektstruktur ### 8.2 Test-Struktur ```python # tests/test_services/test_game_service.py import pytest from services.game_service import GameService class TestGameService: """Tests für den GameService.""" def test_get_by_id_returns_game_when_found(self, db_session): """Gibt ein Spiel zurück, wenn die ID existiert.""" # Arrange service = GameService(db_session) # Act result = service.get_by_id(1) # Assert assert result is not None assert result.name == "Catan" def test_get_by_id_returns_none_when_missing(self, db_session): """Gibt None zurück, wenn die ID nicht existiert.""" service = GameService(db_session) result = service.get_by_id(999999) assert result is None def test_get_by_id_raises_on_negative_id(self, db_session): """Wirft ValueError bei negativer ID.""" service = GameService(db_session) with pytest.raises(ValueError, match="ID darf nicht negativ sein"): service.get_by_id(-1) ``` ### 8.3 Test-Namen - **`test__`** - Deutsch oder Englisch — aber **einheitlich** im gesamten Projekt --- ## 9. Flask-spezifische Regeln ### 9.1 Routes ```python # ✅ Immer `methods` explizit angeben @app.route("/api/games", methods=["GET"]) def list_games() -> tuple[dict, int]: ... # ✅ POST-Daten aus request.get_json(), nicht request.args @app.route("/api/games", methods=["POST"]) def create_game() -> tuple[dict, int]: data = request.get_json(silent=True) if not data: return {"error": "Kein JSON-Body"}, 400 ... # ❌ Falsch — GET-Parameter in POST lesen data = request.args.get("name") # NIEMALS für POST-Daten ``` ### 9.2 Response-Format - Immer **JSON** zurückgeben: `flask.jsonify()` oder `dict` mit Statuscode - Statuscodes **explizit** setzen - Fehler-Responses einheitlich: `{"error": "Nachricht"}` mit passendem Statuscode ```python # ✅ Richtig @app.route("/api/games/") def get_game(game_id: int) -> tuple[dict, int]: game = game_service.get_by_id(game_id) if not game: return {"error": "Spiel nicht gefunden"}, 404 return game.to_dict(), 200 ``` ### 9.3 Config - Flask-Config über `app.config.from_prefixed_env()` (Flask 3.1+) - Secrets nie in `app.config` hartkodieren ```python # ✅ Richtig — Config aus Environment app = Flask(__name__) app.config.from_prefixed_env() # Zugriff: os.environ["FLASK_SECRET_KEY"], os.environ["FLASK_DATABASE_URL"] ``` ### 9.4 Templates - CSS **immer inline** in JCE-Templates (self-contained, keine separaten CSS-Dateien) - Mobile-first Design - BSN-Blau `#20228a` als Primärfarbe - Touch-Buttons min 64px, Overlays min 85vh --- ## 10. Git & Commits ### 10.1 Conventional Commits Jeder Commit folgt dem **Conventional Commits**-Standard: ``` (): [optional body] [optional footer] ``` | Typ | Verwendung | |---|---| | `feat` | Neues Feature | | `fix` | Bugfix | | `docs` | Dokumentation | | `style` | Formatierung (kein Code-Change) | | `refactor` | Code-Umbau (kein Feature, kein Fix) | | `test` | Tests hinzugefügt/geändert | | `chore` | Build, Dependencies, CI | | `perf` | Performance-Verbesserung | | `security` | Sicherheitsrelevante Änderung | **Beispiele:** ``` feat(games): Spiele-Suche mit Volltext-Filter fix(auth): Login bricht bei leeren Feldern nicht mehr ab docs: CODING_STANDARDS.md hinzugefügt chore(deps): Ruff auf 0.15.18 aktualisiert ``` ### 10.2 Commit-Frequenz - **Nach jeder abgeschlossenen Aufgabe** committen - Keine Mega-Commits mit 50 geänderten Dateien - Ein Commit = eine logische Änderung ### 10.3 Vor jedem Commit ```bash pre-commit run # Linting + Formatierung + Typ-Check pytest # Tests müssen grün sein ``` --- ## 11. Toolchain & Durchsetzung ### 11.1 Übersicht | Tool | Zweck | Konfiguration | |---|---|---| | **[Ruff](https://docs.astral.sh/ruff/)** | Linting + Formatierung + Sicherheit | `pyproject.toml` → `[tool.ruff]` | | **[mypy](https://mypy-lang.org/)** | Statische Typ-Prüfung | `pyproject.toml` → `[tool.mypy]` | | **[pre-commit](https://pre-commit.com/)** | Git-Hooks zur Durchsetzung | `.pre-commit-config.yaml` | | **[pytest](https://pytest.org/)** | Testing + Coverage | `pyproject.toml` → `[tool.pytest.ini_options]` | ### 11.2 Quick-Start (neues Projekt) ```bash # 1. Tools installieren pip install ruff mypy pre-commit pytest # 2. Pre-commit-Hooks aktivieren pre-commit install # 3. Erstmalig alles prüfen ruff check . --fix ruff format . mypy . pytest --cov --cov-fail-under=80 # 4. Vor jedem Commit automatisch (Ruff Lint + Format + Basic Checks) git commit -m "feat: ..." # → Ruff prüft + formatiert automatisch # → Merge-Konflikte, Whitespace, Debug-Statements werden geprüft # → mypy läuft nicht in pre-commit (isoliertes venv-Drift) — separat ausführen! ``` ### 11.3 CI-Setup (später) ```yaml # .github/workflows/quality.yml - name: Lint + Security run: ruff check . - name: Type Check (im echten venv!) run: mypy . - name: Tests + Coverage run: pytest --cov --cov-fail-under=80 ``` --- ## Anhang A: Checkliste für Code Reviews Bei jedem Code-Review durchgehen. 🤖 = automatisch von pre-commit erzwungen. - [ ] 🤖 PEP 8 eingehalten? (Ruff-Check bestanden) - [ ] Alle Funktionen haben Typ-Annotationen? (mypy bestanden) - [ ] Docstrings auf Deutsch für alle öffentlichen Funktionen? - [ ] Keine Secrets im Code? - [ ] User-Input validiert? - [ ] Tests geschrieben und grün? Coverage ≥ 80%? - [ ] Keine God-Files (>500 Zeilen)? (manuell) - [ ] Keine God-Funktionen (>50 Statements)? (🤖 Ruff PLR0915) - [ ] McCabe-Komplexität ≤ 10? (🤖 Ruff C901) - [ ] Route enthält keine Business-Logik? - [ ] Commit folgt Conventional Commits? --- ## Anhang B: Warum diese Regeln? > *"Die Basis muss stimmen — wie bei einem Haus, das Fundament ist das wichtigste."* — Daniel Krause Diese Regeln sind nicht willkürlich. Sie basieren auf: 1. **PEP 8** — dem offiziellen Python-Styleguide (seit 2001) 2. **Google Python Style Guide** — bewährt in tausenden Projekten 3. **The Zen of Python** — Guido van Rossums Design-Prinzipien 4. **Erfahrungen aus dem BSN-Chatbot-Projekt** — was funktioniert hat, was nicht 5. **Moderner Tooling-Konsens 2026** — Ruff + mypy + pre-commit sind Industriestandard Jede Regel ist **automatisiert durchsetzbar**. Das bedeutet: Du musst sie nicht im Kopf behalten — die Tools sagen dir, wenn etwas falsch ist. Und sie verhindern, dass fehlerhafter Code überhaupt ins Repository kommt. --- > **Letzte Aktualisierung:** 21. Juni 2026 (extern reviewed ✅ — 6 Fixes eingearbeitet) > **Nächste Überprüfung:** Bei Projektstart oder Dezember 2026 > > **Review-Fixes v1.0:** > 1. Optional[X] → X | None (PEP 604, kein Ruff-Konflikt mehr) > 2. B101 global entfernt — asserts nur in tests/ erlaubt > 3. Bandit-Hook gestrichen — Ruffs S-Regeln reichen > 4. C901 + PLR-Regeln aktiviert — God-Funktionen jetzt automatisch geprüft > 5. mypy aus pre-commit entfernt (isoliertes venv-Drift) > 6. TDD → Empfehlung, Typ-Fixes, Ruff 0.15.18, mypy 2.1.0