In diesem Notebook schauen wir uns an, wie wir die Qualität unseres Codes automatisch sicherstellen können. Wir analysieren dazu das Skript code_quality.py, welches absichtlich verschiedene Fehler und Stil-Mängel enthält.
# Vorbereitung
!uv venv --quiet
!uv sync --quiet
!uv pip install --quiet autoflake bandit beartype black flake8 interrogate isort jaxtyping monkeytype mypy ruff typeguard# Reset Startbedingungen:
!cp code-quality-backup.py code_quality.py
!cp code-quality-better.py code_quality_better.py!cat code_quality.pyimport sys
import os
import random
import numpy as gabelstapler
from dataclasses import dataclass, field
from enum import Enum, auto
class GameException(Exception):
pass
class NotEnoughMoneyException(GameException):
pass
class ProductType(Enum):
FOOD = auto()
def __str__(self):
return self.name.title()
@dataclass
class Inventory:
money: float
stock: dict = {}
def execute_trade(self, product, quantity: int, unit_price):
cost = quantity*unit_price
if quantity > 0:
if self.money < cost:
raise NotEnoughMoneyException(f'Need {cost}€, have {self.money}€')
elif quantity < 0:
current_stock = self.stock.get(product, 0)
if current_stock < abs(quantity):
raise NotEnoughStockException("Not enough stock")
self.money -= cost
self.stock[product] = self.stock.get(product, 0) + quantity
@dataclass
class Market:
product: ProductType
current_price: float
volatility: float
def adjust_price(self, direction: int) -> None:
change = direction * self.volatility
new_price = self.current_price + change
self.current_price = max(1.0, round(new_price, 2))
def run_simulation(starting_money, market_price):
player = Inventory(money=starting_money)
market = Market(product=ProductType.FOOD, current_price=market_price, volatility=0.5)
try:
player.execute_trade(ProductType.FOOD, 5, market.current_price)
except:
print("Etwas ist schiefgelaufen")
return player
if __name__ == "__main__":
final_state = run_simulation(100.0, "10.0")
print(final_state)
Formatter¶
Black¶
Formatter kümmern sich um das Aussehen des Codes (PEP 8). Black ist der bekannteste “uncompromising” Formatter. Wir schauen uns mit --diff an, was Black ändern würde, ohne die Datei direkt zu überschreiben.
!uvx black --diff --color code_quality.py --target-version=py313Installed 7 packages in 6ms
--- code_quality.py 2026-07-01 09:53:02.341896+00:00
+++ code_quality.py 2026-07-01 09:53:03.075483+00:00
@@ -22,46 +22,51 @@
@dataclass
class Inventory:
money: float
- stock: dict = {}
+ stock: dict = {}
def execute_trade(self, product, quantity: int, unit_price):
- cost = quantity*unit_price
+ cost = quantity * unit_price
if quantity > 0:
if self.money < cost:
- raise NotEnoughMoneyException(f'Need {cost}€, have {self.money}€')
+ raise NotEnoughMoneyException(f"Need {cost}€, have {self.money}€")
elif quantity < 0:
current_stock = self.stock.get(product, 0)
if current_stock < abs(quantity):
- raise NotEnoughStockException("Not enough stock")
+ raise NotEnoughStockException("Not enough stock")
self.money -= cost
self.stock[product] = self.stock.get(product, 0) + quantity
+
@dataclass
class Market:
product: ProductType
current_price: float
volatility: float
def adjust_price(self, direction: int) -> None:
- change = direction * self.volatility
+ change = direction * self.volatility
new_price = self.current_price + change
self.current_price = max(1.0, round(new_price, 2))
+
def run_simulation(starting_money, market_price):
player = Inventory(money=starting_money)
- market = Market(product=ProductType.FOOD, current_price=market_price, volatility=0.5)
-
+ market = Market(
+ product=ProductType.FOOD, current_price=market_price, volatility=0.5
+ )
+
try:
player.execute_trade(ProductType.FOOD, 5, market.current_price)
except:
print("Etwas ist schiefgelaufen")
-
+
return player
+
if __name__ == "__main__":
- final_state = run_simulation(100.0, "10.0")
+ final_state = run_simulation(100.0, "10.0")
print(final_state)
would reformat code_quality.py
All done! ✨ 🍰 ✨
1 file would be reformatted.
Wir wollen den Output von Black zur Weiterverarbeitung sichern:
!uvx black code_quality.py --line-length=80 --target-version=py313
!mv code_quality.py code_quality_black.py
!cp code-quality-backup.py code_quality.pyreformatted code_quality.py
All done! ✨ 🍰 ✨
1 file reformatted.
Wenn sich nichts ändert, kann man das auch in Continuous Integration einbauen, indem man mit --check Änderungen vermeidet:
!echo "\nIdempotent:"
!uvx black code_quality_black.py --target-version=py313
!echo "\nCheck nach Black:"
!uvx black --check code_quality_black.py --target-version=py313
!echo "\nCheck, der fehlschlägt:"
!uvx black --check code_quality.py --target-version=py313
Idempotent:
reformatted code_quality_black.py
All done! ✨ 🍰 ✨
1 file reformatted.
Check nach Black:
All done! ✨ 🍰 ✨
1 file would be left unchanged.
Check, der fehlschlägt:
would reformat code_quality.py
Oh no! 💥 💔 💥
1 file would be reformatted.
Man kann mit Black auch Jupyter Notebooks reformatieren:
!uvx --from "black[jupyter]" black --diff --color ../part-practical/practical-1.ipynb --target-version=py313Installed 25 packages in 258ms
--- ../part-practical/practical-1.ipynb 2026-06-10 09:12:20.539137+00:00:cell_1
+++ ../part-practical/practical-1.ipynb 2026-07-01 09:53:05.919385+00:00:cell_1
@@ -6,17 +6,21 @@
from typing import Optional
class GameException(Exception):
"""Base class for all game-related errors."""
+
pass
+
class NotEnoughMoneyException(GameException):
pass
+
class NotEnoughStockException(GameException):
pass
+
class ProductType(Enum):
FOOD = auto()
# Später auch andere Produkte
would reformat ../part-practical/practical-1.ipynb
All done! ✨ 🍰 ✨
1 file would be reformatted.
isort¶
Aus Java-IDEs kennt man vielleicht noch ‘Organize Imports’, was die import-Statements in eine kanonische Reihenfolge bringt (und ungenutzte Imports herauswirft - das passiert hier nicht).
!uvx --from "isort[colors]" isort --diff --color code_quality.py--- /home/voelkel/sciebo/hhu/eipy-26/eipy-skript/part-tools/code_quality.py:before 2026-07-01 11:53:03.657897
+++ /home/voelkel/sciebo/hhu/eipy-26/eipy-skript/part-tools/code_quality.py:after 2026-07-01 11:53:06.257103
@@ -1,9 +1,10 @@
-import sys
import os
import random
-import numpy as gabelstapler
+import sys
from dataclasses import dataclass, field
from enum import Enum, auto
+
+import numpy as gabelstapler
class GameException(Exception):
Konfigurations-Formatter und Linter¶
Wenn man die Projektdaten in einer pyproject.toml verwaltet (wie sich das heutzutage gehört), möchte man diese auch versionieren. Dabei wäre es ungünstig, wenn Permutationen der Zeilenreihenfolge oder Whitespace-Änderungen die Commit History zukleistern und damit die tatsächliche History schwierig nachvollziehbar machen. Um das zu vermeiden, normalisiert man die Reihenfolge und die Whitespace-Platzierungen, entweder per Konvention die von Allen eingehalten werden muss, oder durch ein Programm: einen Konfigurations-Linter bzw. Konfigurations-Formatter (siehe den folgenden Abschnitt für Python-Code-Linter).
Geeignet sind taplo (für Syntax und Formatierung) und toml-sort (für eine alphabetisch konsistente Ordnung), das sind generisch TOML-Werkzeuge.
Spezifisch ist pyproject-fmt für diese Aufgabe. Der Allrounder ruff (s.u.) kann die pyproject.toml nur lintern (Fehler aufdecken), nicht formatieren.
!cp ../pyproject.toml ./
!uvx pyproject-fmt pyproject.toml
!uvx toml-sort --in-place pyproject.toml
!uvx taplo format pyproject.toml
!rm -f pyproject.tomlInstalled 1 package in 6ms.1
no change for pyproject.toml
Installed 2 packages in 8ms
Installed 1 package in 5ms
INFO taplo:format_files:collect_files: found files total=1 excluded=0 cwd="/home/voelkel/sciebo/hhu/eipy-26/eipy-skript/part-tools"
Linter¶
Linter suchen nach logischen Fehlern, ungenutzten Imports oder gefährlichen Patterns (wie dem bare except:).
Flake8¶
Wir betrachten erstmal den Klassiker flake8, der PyFlakes, pycodestyle und McCabe-Komplexitätsanalyse bündelt.
!uvx flake8 code_quality_black.pycode_quality_black.py:1:1: F401 'sys' imported but unused
code_quality_black.py:2:1: F401 'os' imported but unused
code_quality_black.py:3:1: F401 'random' imported but unused
code_quality_black.py:4:1: F401 'numpy as gabelstapler' imported but unused
code_quality_black.py:5:1: F401 'dataclasses.field' imported but unused
code_quality_black.py:34:80: E501 line too long (82 > 79 characters)
code_quality_black.py:38:23: F821 undefined name 'NotEnoughStockException'
code_quality_black.py:64:5: E722 do not use bare 'except'
Die Fehlercodes F401 und E501 kommen von pycodestyle, wobei E für einen Style Error steht und F für einen Fehler, den PyFlakes eingeführt hat. Es gibt zudem noch z.B. I für Probleme mit imports (isort), D für Docstring-Probleme (pydocstyle), UP für Upgrade-Empfehlungen (pyugrade).
Wir können auch schauen, welche Warnungen wir bereits durch die Formatierung mit Black erledigt haben:
%%bash
run_linter() {
uvx flake8 --format="%(row)d:%(col)d: %(code)s %(text)s" "$1"
}
diff \
<(run_linter code_quality_black.py) \
<(run_linter code_quality.py) || true5a6
> 27:21: W291 trailing whitespace
8c9,18
< 64:5: E722 do not use bare 'except'
---
> 38:66: W291 trailing whitespace
> 43:1: E302 expected 2 blank lines, found 1
> 50:45: W291 trailing whitespace
> 54:1: E302 expected 2 blank lines, found 1
> 56:80: E501 line too long (89 > 79 characters)
> 57:1: W293 blank line contains whitespace
> 60:5: E722 do not use bare 'except'
> 62:1: W293 blank line contains whitespace
> 65:1: E305 expected 2 blank lines after class or function definition, found 1
> 66:48: W291 trailing whitespace
Autoflake¶
Das Werkzeug Autoflake soll manche Probleme automatisch beheben:
!uvx autoflake --remove-all-unused-imports code_quality.py--- original/code_quality.py
+++ fixed/code_quality.py
@@ -1,8 +1,4 @@
-import sys
-import os
-import random
-import numpy as gabelstapler
-from dataclasses import dataclass, field
+from dataclasses import dataclass
from enum import Enum, auto
!uvx autoflake --in-place --remove-all-unused-imports code_quality_black.py
!uvx autoflake --verbose --verbose --remove-all-unused-imports code_quality_black.pyClean code_quality_black.py: nothing to fix
Pylint¶
Ein eher pedantisches Werkzeug, um Konventionen zu prüfen. Vergibt eine Punktzahl, sodass man vergleichen kann, ob Änderungen am Code die so gemessene Code-Qualität verbessern oder verschlechtern.
!uvx pylint code_quality_black.py --output-format colorized --enable-all-extensionsInstalled 7 packages in 8ms
************* Module code_quality_black
code_quality_black.py:30:0: C0301: Line too long (82/80) (line-too-long)
code_quality_black.py:31:8: R5601: Consecutive elif with differing indentation level, consider creating a function to separate the inner elif (confusing-consecutive-elif)
code_quality_black.py:33:15: R6103: Use 'if (current_stock := self.stock.get(product, 0)) < abs(quantity):' instead (consider-using-assignment-expr)
code_quality_black.py:34:22: E0602: Undefined variable 'NotEnoughStockException' (undefined-variable)
code_quality_black.py:60:4: W0702: No exception type(s) specified (bare-except)
------------------------------------------------------------------
Your code has been rated at 7.91/10 (previous run: 7.91/10, +0.00)
Dabei haben wir sogar einige sehr nervige Meldungen unterdrückt, indem in der pyproject.toml dieser Abschnitt steht:
[tool.pylint.messages_control]
disable = [
"missing-module-docstring", # C0114
"missing-class-docstring", # C0115
"missing-function-docstring", # C0116
"too-few-public-methods", # R0903
"invalid-name", # C0103
]Und wir können auch Inline im Code Pylint-Warnungen unterdrücken, wie in diesem Beispiel:
def alte_funktion(a, b, c, d, e, f, g): # pylint: disable=too-many-arguments
# pylint: disable=invalid-name
x = a + b
return xDas ist dann sinnvoll, wenn wir z.B. die Methodensignatur nicht ändern können, weil eine alte API das so vorgibt.
Die Fehlercodes von Pylint sind nicht genau die selben wie bei Flake8, so steht z.B. C für Code Convention (also Stil), was bei Pylint bzw. pycodestyle E und F waren. Refactoring-Empfehlungen beginnen mit R.
FawltyDeps¶
FawltyDeps ist ein Dependency Linter. Es wird systematisch in .py und .ipynb nach Import-Deklarationen gesucht und mit Projektdateien wie requirements.txt oder pyproject.toml abgeglichen. Damit findet man Pakete, die man noch nicht explizit als Abhängigkeit deklariert hatte, sowie Pakete, deren Abhängigkeit im Code gar nicht gegeben ist, aber fälschlicherweise für das Paket deklariert wurde.
uvx fawltydepsBandit¶
Es gibt auch ‘Linter’ mit dem Ziel, Sicherheitslücken bzw. problematische Muster wie hartkodierte Passwörter zu finden. Wir stellen fest, dass der bisher besprochene Code nicht problematisch ist:
!uvx bandit --quiet -r code_quality_better.py⠙ Das folgende ist ein Beispiel für Probleme, die Bandit aufdecken kann:
%%writefile bad_security.py
def calculate_dynamic_price(base_price, user_formula):
# GEFÄHRLICH: Code-Injection möglich
return base_price * eval(user_formula)
API_SECRET = "super_secret_token_12345"Writing bad_security.py
!uvx bandit -r bad_security.py[main] INFO profile include tests: None
[main] INFO profile exclude tests: None
[main] INFO cli include tests: None
[main] INFO cli exclude tests: None
[main] INFO running on Python 3.13.2
Run started:2026-07-01 09:53:11.509096+00:00
Test results:
>> Issue: [B307:blacklist] Use of possibly insecure function - consider using safer ast.literal_eval.
Severity: Medium Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
More Info: https://bandit.readthedocs.io/en/1.9.4/blacklists/blacklist_calls.html#b307-eval
Location: ./bad_security.py:3:24
2 # GEFÄHRLICH: Code-Injection möglich
3 return base_price * eval(user_formula)
4
--------------------------------------------------
>> Issue: [B105:hardcoded_password_string] Possible hardcoded password: 'super_secret_token_12345'
Severity: Low Confidence: Medium
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
More Info: https://bandit.readthedocs.io/en/1.9.4/plugins/b105_hardcoded_password_string.html
Location: ./bad_security.py:5:13
4
5 API_SECRET = "super_secret_token_12345"
--------------------------------------------------
Code scanned:
Total lines of code: 3
Total lines skipped (#nosec): 0
Run metrics:
Total issues (by severity):
Undefined: 0
Low: 1
Medium: 1
High: 0
Total issues (by confidence):
Undefined: 0
Low: 0
Medium: 1
High: 1
Files skipped (0):
Interrogate¶
Wir hatten zuvor die pedantischen Meldungen über Docstrings abgeschaltet, würden aber evtl. doch eine Übersicht haben wollen, wie viel des Projekts dokumentiert ist:
!uvx interrogate code_quality_better.py --colorInstalled 6 packages in 5ms
RESULT: FAILED (minimum: 80.0%, actual: 9.1%)
Type Checker¶
Während Linters die Struktur prüfen, sichern Type-Checker die Typ-Konsistenz. Neben dem De-facto-Standard Mypy hat sich das von Microsoft entwickelte pyright durch extrem schnelle Analysezeiten etabliert. Jüngere Entwicklungen wie ty (astral-sh/ty) von den Machern von uv/ruff oder das darauf aufbauende pyrefly (Vergleich) experimentieren mit noch besserer Performance und tieferer Integration in moderne Toolchains (ähnlich wie Ruff es für Flake8/Black getan hat). Dabei geht der Trend zu eher strengeren Typ-Überprüfungen.
Wenn man die grundlegende Logik von mypy verstanden hat, lässt sich ein Projekt später leicht auf pyrefly umstellen. Generell kann man als Faustregel nutzen: sollen andere Entwickler den Code nutzen (als import), so sollte man eher strenge Typenchecks verwenden, ansonsten kann man es auch etwas lockerer handhaben.
Mypy¶
!uvx mypy code_quality_black.pyInstalled 6 packages in 28ms
code_quality_black.py:34: error: Name "NotEnoughStockException" is not defined; did you mean "NotEnoughMoneyException"? [name-defined]
Found 1 error in 1 file (checked 1 source file)
Damit wir uns nicht von der Fehlermeldung verwirren lassen, betrachten wir eine Version ohne den Fehler (wo die fehlende Exception deklariert ist):
!echo "Geänderter Code:"
!diff code_quality_black.py code_quality_better.py
!echo "Output von mypy:"
!uvx mypy code_quality_better.pyGeänderter Code:
1c1
< from dataclasses import dataclass
---
> from dataclasses import dataclass, field
12a13,17
> class NotEnoughStockException(GameException):
> """If stock runs low..."""
> pass
>
>
23c28,29
< stock: dict = {}
---
> # dataclasses throw ValueError on using the same {}
> stock: dict = field(default_factory=dict)
Output von mypy:
Success: no issues found in 1 source file
Wir erinnern uns aber daran, dass der Code z.B. diese Zeile enthält:
def execute_trade(self, product, quantity: int, unit_price):Da fehlen offensichtlich Type Hints bei product und unit_price, aber Mypy hat sich nicht beschwert (und bisher auch sonst kein Linter). Das lässt sich explizit einfordern:
!uvx mypy code_quality_better.py --strictcode_quality_better.py:21: error: Function is missing a type annotation [no-untyped-def]
code_quality_better.py:29: error: Missing type arguments for generic type "dict" [type-arg]
code_quality_better.py:31: error: Function is missing a return type annotation [no-untyped-def]
code_quality_better.py:31: error: Function is missing a type annotation for one or more parameters [no-untyped-def]
code_quality_better.py:58: error: Function is missing a type annotation [no-untyped-def]
code_quality_better.py:73: error: Call to untyped function "run_simulation" in typed context [no-untyped-call]
Found 6 errors in 1 file (checked 1 source file)
Wir können auch Codeblöcke explizit von den Typechecker Warnungen ausschließen, wie in diesem Beispiel:
final_state = run_simulation(100.0, "10.0") # type: ignore[arg-type]
Monkeytype¶
Da Python dynamisch ist, ist es schwierig über statische Analyse die Typen abzuleiten. Also führen wir das Programm doch einmal aus!
!python code_quality_better.pyEtwas ist schiefgelaufen
Inventory(money=100.0, stock={})
Das Modul Monkeytype versucht, während der Ausführung die Typinformation zu raten:
!uvx monkeytype run code_quality_better.py
!uvx monkeytype stub code_quality_betterEtwas ist schiefgelaufen
Inventory(money=100.0, stock={})
No traces found for module code_quality_better
Da Monkeytype Code aus __main__ ignoriert, muss man einen Workaround nutzen, ein separates Runner Script:
%%writefile tmp_run.py
from code_quality_better import run_simulation
if __name__ == "__main__":
final_state = run_simulation(100.0, 10.0)Writing tmp_run.py
!uvx monkeytype run tmp_run.py
!uvx monkeytype stub code_quality_better
!uvx monkeytype apply code_quality_betterdef run_simulation(starting_money: float, market_price: float) -> Inventory: ...
class Inventory:
def execute_trade(self, product: ProductType, quantity: int, unit_price: float): ...
from dataclasses import dataclass, field
from enum import Enum, auto
class GameException(Exception):
pass
class NotEnoughMoneyException(GameException):
pass
class NotEnoughStockException(GameException):
"""If stock runs low..."""
pass
class ProductType(Enum):
FOOD = auto()
def __str__(self):
return self.name.title()
@dataclass
class Inventory:
money: float
# dataclasses throw ValueError on using the same {}
stock: dict = field(default_factory=dict)
def execute_trade(self, product: ProductType, quantity: int, unit_price: float):
cost = quantity * unit_price
if quantity > 0:
if self.money < cost:
raise NotEnoughMoneyException(f"Need {cost}€, have {self.money}€")
elif quantity < 0:
current_stock = self.stock.get(product, 0)
if current_stock < abs(quantity):
raise NotEnoughStockException("Not enough stock")
self.money -= cost
self.stock[product] = self.stock.get(product, 0) + quantity
@dataclass
class Market:
product: ProductType
current_price: float
volatility: float
def adjust_price(self, direction: int) -> None:
change = direction * self.volatility
new_price = self.current_price + change
self.current_price = max(1.0, round(new_price, 2))
def run_simulation(starting_money: float, market_price: float) -> Inventory:
player = Inventory(money=starting_money)
market = Market(
product=ProductType.FOOD, current_price=market_price, volatility=0.5
)
try:
player.execute_trade(ProductType.FOOD, 5, market.current_price)
except:
print("Etwas ist schiefgelaufen")
return player
if __name__ == "__main__":
final_state = run_simulation(100.0, "10.0")
print(final_state)
Das fügt immer noch nicht alle Type Hints hinzu, die wir uns wünschen könnten, weil es Code gibt, der von unserem Runner nicht erreicht wird.
Laufzeit-Typüberprüfung¶
Mypy prüft Typen statisch, d.h. ohne Ausführung des Programms. In der Praxis stehen Dimensionen von Matrizen oder Tensoren manchmal erst zur Laufzeit fest. Die Bilbiothek typeguard erzwingt diese Checks während der Ausführung. Das Anwendungsgebiet von typeguard, Überprüfung, ist übrigens zu unterscheiden von der Konvertierung von Daten, wie es Pydantic macht (siehe späteres Kapitel).
%%writefile simple_typeguard.py
from typeguard import typechecked
@typechecked
def process_scores(scores: list[int]) -> int:
"""Erwartet eine Liste, die ausschließlich Integer enthält."""
return sum(scores)
if __name__ == "__main__":
print("Korrekt:", process_scores([10, 20, 30]))
try:
# Provoziert einen Fehler: Float statt Int in der Liste
process_scores([10, 20.5, 30])
except Exception as e:
print("\nTyp-Fehler abgefangen:\n", type(e).__name__)
print(e)Writing simple_typeguard.py
!uv run python simple_typeguard.pyKorrekt: 60
Typ-Fehler abgefangen:
TypeCheckError
the return value (float) is not an instance of int
Wenn man wirklich Array-Dimensionen spezifizieren möchte, kann jaxtyping hilfreich sein. Da die Typenprüfung von typeguard einer Liste der Länge in läuft, verwendet man für große Daten eher den drop-in Ersatz beartype, in dem Listen nur stichprobenartig geprüft werden. Das ist zur Laufzeit oft hinreichend, während in einer kontrollierten Test-Umgebung dann wiederum typeguard aufgrund der Reproduzierbarkeit von Vorteil ist.
%%writefile runtime_types.py
import numpy as np
from jaxtyping import Float, jaxtyped, TypeCheckError
from beartype import beartype
@jaxtyped(typechecker=beartype)
def calculate_batch_cost(prices: Float[np.ndarray, "N"], quantities: Float[np.ndarray, "N"]) -> Float[np.ndarray, "N"]:
return prices * quantities
if __name__ == "__main__":
p = np.array([10.5, 20.0])
q_correct = np.array([2.0, 3.5])
q_wrong = np.array([[1.0], [2.0]])
a, b = calculate_batch_cost(p, q_correct)
assert a == p[0]*q_correct[0] and b == p[1]*q_correct[1]
a, b = p * q_wrong # ohne Prüfung!
try:
a_new, b_new = calculate_batch_cost(p, q_wrong)
except TypeCheckError:
print(f"Shape-Fehler abgefangen: {q_wrong.shape} passt nicht zum erwarteten {p.shape}.")
print(a, "!=", p[0]*q_wrong[0])Overwriting runtime_types.py
!uv run python runtime_types.pyShape-Fehler abgefangen: (2, 1) passt nicht zum erwarteten (2,).
[10.5 20. ] != [10.5]
Ein (2, 1)-Array ist eine -Matrix und kein flaches 1D-Array; die strikte Typüberprüfung verhindert hier ein fehlerhaftes, stillschweigendes numpy-Broadcasting. Der Output wäre ein (2, 2)-Array, also eine -Matrix, kein 1D-Array. Ohne die Typüberprüfung kann das Broadcasting solche Fehler also weit weg transportieren.
Ruff¶
Ruff ist in Rust geschrieben und extrem schnell. Es ersetzt Flake8, isort, Black und viele mehr. In Projekten mit und ohne uv lässt es sich direkt via uvx ruff ausführen. Dabei ist ruff format als Drop-in Ersatz für den Formatter Black gedacht und ruff check ist der eigentliche Linter.
print("--- Ruff Check (Linting) ---")
!uvx ruff check code_quality_better.py --color never --no-cache
print("\n--- Ruff Format (Diff) ---")
!uvx ruff format --diff code_quality_better.py--- Ruff Check (Linting) ---
Installed 1 package in 3ms
E722 Do not use bare `except`
--> code_quality_better.py:66:5
|
64 | try:
65 | player.execute_trade(ProductType.FOOD, 5, market.current_price)
66 | except:
| ^^^^^^
67 | print("Etwas ist schiefgelaufen")
|
Found 1 error.
--- Ruff Format (Diff) ---
--- code_quality_better.py
+++ code_quality_better.py
@@ -12,6 +12,7 @@
class NotEnoughStockException(GameException):
"""If stock runs low..."""
+
pass
1 file would be reformatted
Man kann bei jedem brauchbaren Linter gezielt Warnungen deaktivieren, bei Ruff mit noqa: CODE, hier E722 für den Bare-Except-Fehler (noqa = No Quality Assurance):
try:
player.execute_trade(ProductType.FOOD, 5, market.current_price)
except: # noqa: E722
print("Etwas ist schiefgelaufen")Etwas ist schiefgelaufen
Wie beim graduellen Hinzufügen von Typeninformationen kann man auch das Lintern des Codes nach und nach machen, indem man z.B. für eine ganze Datei Fehlercodes ausschließt mit einem Kommentar # ruff: noqa: CODE am Anfang der Datei.
Die Fehlercode-Systematik übernimmt Ruff von Flake8, wobei Pylint-Fehler auch mit dem Prefix PL verwendet werden können (etwa in noqa-Anweisungen).
Automatisierung¶
Wir wollen Code-Qualitätstests nicht bei jeder Änderung manuell im Terminal anstoßen. In der Praxis greifen Automatisierungen auf verschiedenen Ebenen ineinander:
IDE-Integration: Moderne Entwicklungsumgebungen formatieren den Code oft bereits beim Speichern (Format on Save), wenn man das möchte. Dadurch werden Stil-Konflikte (unnötige Diffs) in der Versionskontrolle minimiert.
Lokale Automatisierung (Pre-Commit-Hooks): Um zu verhindern, dass Linter-Fehler überhaupt erst in die Versionskontrolle gelangen, verankert man die Checks im Git-Workflow.
Serverseitige Automatisierung (CI/CD): CI-Systeme wie GitLab CI oder GitHub Actions führen Linter und Tests bei jedem Push in sauberen Containern aus. Das garantiert Reproduzierbarkeit unabhängig vom Rechner der Entwickler*innen.
pre-commit¶
Das Tool pre-commit verwaltet Skripte, die Git automatisch vor jedem git commit ausführt. Schlägt ein Test fehl, wird der Commit abgebrochen. Die Konfiguration erfolgt über eine Datei namens .pre-commit-config.yaml im Hauptverzeichnis des Projekts.
Ein typisches Setup, das Standard-Fehler behebt und Ruff für Linting und Formatierung nutzt, sieht so aus:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
# Linter ausführen (und beheben, falls möglich)
- id: ruff
args: [ --fix ]
# Formatter ausführen
- id: ruff-formatund uvx pre-commit install installiert den pre-commit-hook so, dass git commit ihn auslöst.
Continuous Integration¶
Serverseitig wird z.B. die .gitlab-ci.yml so konfiguriert:
stages:
- lint
lint_code:
stage: lint
image: python:3.13-slim
script:
- pip install uv
- uv pip install --system ruff mypy
# Checks durchführen (Fehler führen zum Abbruch der Pipeline)
- ruff check .
- ruff format --check .
- mypy .Dabei ist python:3.13-slim ein Docker-Image, also reproduzierbar und sandboxed.
Zum Ende des Notebooks räumen wir die temporären Dateien wieder auf:
!rm -f bad_security.py code_quality.py code_quality.py.bak code_quality_better.py code_quality_black.py monkeytype.sqlite3 runtime_types.py simple_typeguard.py tmp_run.py