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.
Code wird 10Γ ΓΆfter gelesen als geschrieben. Jede Zeile muss fΓΌr einen anderen Entwickler (oder dein zukΓΌnftiges Ich) sofort verstΓ€ndlich sein.
Code ist Kommunikation. Unklares wird umgeschrieben β nicht mit Kommentar βerklΓ€rt".
Kein Magic. Typen, Parameter, RΓΌckgabewerte β alles sichtbar.
YAGNI β keine Abstraktionen fΓΌr hypothetische Zukunft.
Exceptions nie still schlucken. Spezifisch fangen + loggen.
Durchsetzung: ruff check + ruff format AUTO
Maximal 100 Zeichen. 88 (Black-Default) ist zu knapp fΓΌr Flask-Routen und SQLAlchemy-Queries.
# β
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()
") fΓΌr Strings" enthΓ€lt"""triple double"""def get_games() -> list[dict[str, object]]:
"""Holt alle Spiele aus der Datenbank."""
...
Reihenfolge (automatisch): 1. Standard-Library 2. Drittanbieter 3. Projekt-intern
from module import * Β· ZirkulΓ€re ImportsImmer bei mehrzeiligen Collections.
items = ["Brettspiel", "Kartenspiel", "WΓΌrfelspiel",]
Durchsetzung: mypy --strict AUTO
Jede Funktion und Methode MUSS vollstΓ€ndige Typ-Annotationen haben. Keine Ausnahmen.
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):
...
| 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): |
X | None und nicht Optional[X]?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"]).
| 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(): |
is_active, has_permission, should_retryget_, create_, validate_Code-SchlΓΌsselwΓΆrter sind Englisch. Alle menschenlesbaren ErklΓ€rungen: Deutsch.
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)
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.
| 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) |
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
app.py β 6000 Zeilen. Auslagern!
Request β Service β Response. Nichts weiter.
Validierung, Berechnung, DB-Zugriffe.
Tabellen-Definition. Keine Business-Logik.
Durchsetzung: ruff (S-Regeln = flake8-bandit) AUTO
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.
.env + .db in .gitignore.
assert-Statements in Produktionscode AUTOpickle fΓΌr ungetrustete Datensubprocess mit shell=False (Default)bcrypt oder argon2 β nie md5/sha1Durchsetzung: pytest --cov --cov-fail-under=80 AUTO
pytest als einziges Test-Frameworktests/, Struktur spiegelt src/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"
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
app.config.from_prefixed_env() β Secrets nie hartkodieren<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 |
pre-commit run + pytestmypy . im
Projekt-venv. Stattdessen: mypy lΓ€uft lokal (Schritt 4 im Workflow)
und in CI gegen das echte venv.S-Regeln (flake8-bandit) decken dieselben Sicherheits-Checks ab
β ein Tool weniger, keine widersprΓΌchlichen Findings.
Linting + Formatierung
10-100Γ schneller als Flake8+Black
Statische Typ-PrΓΌfung
β οΈ Major v2 (Mai 2026) β bei Problemen 1.20
Git-Hooks
Blockiert Commits bei VerstΓΆΓen
Testing + Coverage
80% Coverage minimum
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.
# 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)
| 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 |
Diese Checks sind manuell im Review durchzufΓΌhren oder laufen in CI:
pytest --cov muss manuell ausgefΓΌhrt werden (oder in CI)# .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
Bei jedem Code-Review durchgehen. AUTO = wird von pre-commit erzwungen.
from module import *?pdb, breakpoint)?Diese Dateien werden im Projekt-Root abgelegt. Alle Review-Fixes sind eingearbeitet.
[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",
]
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)"
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 |