πŸ”’ 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

# βœ… 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

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 | Nonestr | NoneOptional[str]
Built-in Generics (3.9+)list[dict[str, object]]List[Dict]
Generics parametrisierendict[str, Any]dict (implizites dict[Any, Any])
Explizites -> Nonedef 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

ElementKonventionBeispiel
Module / Packagessnake_casegame_service.py
KlassenPascalCaseGameRepository
Funktionen / Methodensnake_caseget_game_by_id()
Variablensnake_casegame_count
KonstantenUPPER_SNAKE_CASEMAX_RESULTS = 50
Private (intern)_leading_underscoredef _parse_response():

ZusΓ€tzlich

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

EinheitMaximumPrΓΌfung
Modul (.py-Datei)500 ZeilenMANUELL (Review)
Funktion / Methode50 StatementsAUTO (Ruff PLR0915)
KomplexitΓ€t (McCabe)max 10AUTO (Ruff C901)
Klasse300 ZeilenMANUELL (Review)
Zeile100 ZeichenAUTO (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

7.3 Weitere Regeln

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 + Statuscodereturn data, 201return data
Fehler einheitlich{"error": "msg"}, 400plain 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

10 Git & Commits

Conventional Commits

<type>(<scope>): <description>
TypVerwendung
featNeues Feature
fixBugfix
docsDokumentation
styleFormatierung (kein Code-Change)
refactorCode-Umbau
testTests
choreBuild, Deps, CI
securitySicherheitsrelevante Γ„nderung

Commit-Hygiene

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?

HookPrΓΌftBlockiert bei
Ruff (Lint)Code-Stil, Fehler, KomplexitÀt, SicherheitPEP-8-Verstâßen, ungenutzten Imports, S-Regeln
Ruff (Format)EinrΓΌckung, Quotes, ZeilenlΓ€ngeAbweichender Formatierung
Check-Merge-ConflictMerge-Konflikte<<<<<<< im Code
Trailing-WhitespaceWhitespace am ZeilenendeLeerzeichen/Tabs am Zeilenende
End-of-File-FixerFehlende Newline am DateiendeDateien ohne abschließende Newline
Debug-Statementspdb, breakpoint()Debug-Code im Commit
Large FilesDateigrâße > 5 MBVersehentliche Binaries

11.3 Was wird NICHT automatisch geprΓΌft?

Diese Checks sind manuell im Review durchzufΓΌhren oder laufen 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.

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.

#ProblemFixBetroffene 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