19 KiB
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
- Allgemeine Prinzipien
- Code-Stil
- Typ-Annotationen
- Namenskonventionen
- Kommentare & Docstrings
- Projektstruktur & Architektur
- Sicherheit
- Testing
- Flask-spezifische Regeln
- Git & Commits
- Toolchain & Durchsetzung
1. Allgemeine Prinzipien
1.1 Das Zen of Python (PEP 20)
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
# ✅ 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)
# ✅ 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):
- Standard-Library (
os,json,datetime) - Drittanbieter (
flask,sqlalchemy,requests) - Projekt-interne (
from .models import ...)
# ✅ 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
# ✅ 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.
# ✅ 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 |
|---|---|---|
Optional statt X | None (Konsistenz) |
Optional[str] |
str | None |
list/dict statt List/Dict (Python 3.9+) |
list[str] |
List[str] |
Any nur in Ausnahmefällen |
dict[str, Any] (API-Response) |
Any als Rückgabetyp |
-> None bei Funktionen ohne Return |
def log(msg: str) -> None: |
def log(msg: str): |
from typing import Optional, Any
# ✅ Richtig
def find_user(email: str) -> Optional[dict[str, Any]]:
"""Sucht einen User per E-Mail. Gibt None zurück, wenn nicht gefunden."""
...
# ✅ Richtig — Flask-Route
@app.route("/api/games/<int:game_id>")
def get_game(game_id: int) -> tuple[dict, 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_countstattuc,responsestattresp
5. Kommentare & Docstrings
5.1 Sprache
Alle Kommentare und Docstrings werden auf DEUTSCH geschrieben.
# ✅ 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):
def komplexe_funktion(
name: str,
werte: list[int],
optional: Optional[str] = None,
) -> dict[str, Any]:
"""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
# ✅ 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.pydarf 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
# ✅ Richtig — Route delegiert an Service
@app.route("/api/games/<int:game_id>")
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/<int:game_id>")
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.
# ✅ 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)
# ✅ 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
-Odeaktiviert) - Keine
picklefür ungetrustete Daten subprocessmitshell=False(Default)- Hash-Funktionen für Passwörter:
bcryptoderargon2, niemd5/sha1
8. Testing
Durchsetzung: pytest mit Coverage-Check
8.1 Grundregeln
- Jede neue Funktion → Test (TDD: erst Test, dann Code)
- 80% Code Coverage als Minimum
- pytest als Test-Framework
- Tests in
tests/, spiegelt die Projektstruktur
8.2 Test-Struktur
# 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_<was>_<erwartetes_ergebnis>- Deutsch oder Englisch — aber einheitlich im gesamten Projekt
9. Flask-spezifische Regeln
9.1 Routes
# ✅ 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()oderdictmit Statuscode - Statuscodes explizit setzen
- Fehler-Responses einheitlich:
{"error": "Nachricht"}mit passendem Statuscode
# ✅ Richtig
@app.route("/api/games/<int:game_id>")
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.confighartkodieren
# ✅ 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
#20228aals Primärfarbe - Touch-Buttons min 64px, Overlays min 85vh
10. Git & Commits
10.1 Conventional Commits
Jeder Commit folgt dem Conventional Commits-Standard:
<type>(<scope>): <description>
[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.11.3 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
pre-commit run # Linting + Formatierung + Typ-Check
pytest # Tests müssen grün sein
11. Toolchain & Durchsetzung
11.1 Übersicht
| Tool | Zweck | Konfiguration |
|---|---|---|
| Ruff | Linting + Formatierung | pyproject.toml → [tool.ruff] |
| mypy | Statische Typ-Prüfung | pyproject.toml → [tool.mypy] |
| pre-commit | Git-Hooks zur Durchsetzung | .pre-commit-config.yaml |
| pytest | Testing | pyproject.toml → [tool.pytest.ini_options] |
| Bandit | Sicherheits-Scans | pyproject.toml → [tool.bandit] |
11.2 Quick-Start (neues Projekt)
# 1. Tools installieren
pip install ruff mypy pre-commit pytest bandit
# 2. Pre-commit-Hooks aktivieren
pre-commit install
# 3. Erstmalig alles prüfen
ruff check . --fix
ruff format .
mypy .
pytest
# 4. Vor jedem Commit automatisch
git commit -m "feat: ..."
# → Ruff prüft + formatiert automatisch
# → mypy prüft die Typen
# → Bandit scannt auf Sicherheitslücken
11.3 CI-Setup (später)
# .github/workflows/quality.yml
- name: Lint
run: ruff check .
- name: Type Check
run: mypy .
- name: Security Scan
run: bandit -r src/ -c pyproject.toml
- name: Tests
run: pytest --cov --cov-fail-under=80
Anhang A: Checkliste für Code Reviews
- 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?
- Keine God-Files (>500 Zeilen)?
- Keine God-Funktionen (>50 Zeilen)?
- 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:
- PEP 8 — dem offiziellen Python-Styleguide (seit 2001)
- Google Python Style Guide — bewährt in tausenden Projekten
- The Zen of Python — Guido van Rossums Design-Prinzipien
- Erfahrungen aus dem BSN-Chatbot-Projekt — was funktioniert hat, was nicht
- 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 Nächste Überprüfung: Bei Projektstart oder nach 6 Monaten