Python Assert: intelligenter debuggen
Aktualisiert am
Du debugst eine Datenverarbeitungsfunktion. Irgendwo zwischen dem Einlesen der CSV und dem Schreiben der Ausgabe wurde aus einer Spalte mit ganzen Zahlen plötzlich eine Spalte mit Fließkommazahlen, eine Liste, die niemals leer sein sollte, ist leer geworden, und eine Benutzer-ID, die positiv sein sollte, ist jetzt -1. Du verteilst überall print()-Statements, startest das Skript erneut, starrst auf die Ausgabe und wiederholst das Ganze. Eine Stunde später findest du den Fehler -- eine einzelne Funktion hat stillschweigend ungültige Eingaben akzeptiert und beschädigte Daten weitergereicht. Der eigentliche Fehler passierte fünfzig Zeilen vor dem Absturz, und nichts hat dich darauf hingewiesen.
Das ist ein universelles Problem in der Python-Entwicklung. Bugs breiten sich stillschweigend aus. Ein None-Wert schlüpft durch drei Funktionsaufrufe, bevor er einen AttributeError auslöst. Ein negativer Array-Index greift auf das falsche Element zu. Ein Dictionary, das fünf Schlüssel haben sollte, hat nur vier, und der fehlende Schlüssel verursacht einen subtilen Logikfehler, der erst in Produktion sichtbar wird. Wenn der Fehler sichtbar wird, hast du bereits den Kontext verloren, an der es eigentlich schiefgelaufen ist.
Pythons assert-Anweisung löst dieses Problem, indem sie Bugs genau an der Stelle des Fehlers abfängt, mit einer klaren Meldung darüber, was falsch gelaufen ist. Statt darauf zu hoffen, dass schlechte Daten irgendwann zu einem offensichtlichen Absturz führen, deklarierst du deine Annahmen explizit -- und Python erzwingt sie sofort.
Was ist die Assert-Anweisung?
Die assert-Anweisung prüft eine Bedingung. Wenn die Bedingung True ist, passiert nichts. Wenn die Bedingung False ist, löst Python sofort eine AssertionError aus.
assert 2 + 2 == 4 # Passes silently
assert 2 + 2 == 5 # Raises AssertionErrorDie grundlegende Syntax lautet:
assert condition
assert condition, "Error message explaining what went wrong"Intern übersetzt Python assert in eine if-Anweisung. Das ist das genaue Äquivalent:
# assert condition, message
# is equivalent to:
if __debug__:
if not condition:
raise AssertionError(message)Die Variable __debug__ ist standardmäßig True. Sie wird nur dann False, wenn Python mit dem -O-Flag (optimiert) ausgeführt wird. Das bedeutet, dass Assertions in der Produktion vollständig deaktiviert werden können -- eine Eigenschaft mit wichtigen Konsequenzen, auf die wir später eingehen.
Hier ist, was passiert, wenn eine Assertion fehlschlägt:
x = -1
assert x >= 0, f"Expected non-negative value, got {x}"Output:
Traceback (most recent call last):
File "example.py", line 2, in <module>
assert x >= 0, f"Expected non-negative value, got {x}"
AssertionError: Expected non-negative value, got -1Der Traceback zeigt genau auf die Zeile, in der die Annahme verletzt wurde, und die Meldung sagt dir genau, was schiefgelaufen ist. Vergleiche das mit einem rätselhaften IndexError zwanzig Zeilen später, weil negative Werte unkontrolliert weitergereicht wurden.
Grundlegende Assert-Verwendung
Einfache Assertions
Die einfachsten Assertions prüfen eine einzelne Bedingung:
# Check that a variable is not None
config = load_config()
assert config is not None
# Check that a list is not empty
items = get_items()
assert len(items) > 0
# Check a mathematical property
result = calculate_discount(price=100, percent=20)
assert result == 80Assertions mit benutzerdefinierten Meldungen
Füge immer eine Meldung hinzu. Ohne sie liefert eine fehlgeschlagene Assertion kaum Kontext:
# Bad: no message
assert len(users) > 0
# Good: descriptive message
assert len(users) > 0, "User list is empty -- database query may have failed"
# Good: include the actual value
assert temperature >= -273.15, f"Temperature {temperature}C is below absolute zero"Die Meldung ist das zweite Argument von assert, getrennt durch ein Komma. Sie kann jeder Ausdruck sein, der einen String erzeugt, einschließlich f-Strings mit Laufzeitwerten:
def process_batch(items, batch_size):
assert batch_size > 0, f"batch_size must be positive, got {batch_size}"
assert len(items) >= batch_size, (
f"Not enough items: need {batch_size}, have {len(items)}"
)
# Process the batch...Assertions mit Klammern -- Eine häufige Falle
Es gibt einen subtilen Fehler, der viele Python-Entwickler stolpern lässt:
# WARNING: This assertion NEVER fails!
assert(condition, "error message")Dadurch wird ein Tupel (condition, "error message") erzeugt. Ein nicht-leeres Tupel ist immer wahrheitswertig, also besteht die Assertion immer. Python warnt dich sogar:
SyntaxWarning: assertion is always true, perhaps remove parentheses?Die richtige Form ist:
# Correct: no parentheses
assert condition, "error message"
# Also correct: parentheses only around the condition
assert (condition), "error message"
# Also correct: multi-line with implicit line continuation
assert (
very_long_condition_that_needs_wrapping
), "error message"Assert mit komplexen Bedingungen
Mehrere Bedingungen
Du kannst Bedingungen mit and, or und not kombinieren:
def create_user(name, age, email):
assert name and isinstance(name, str), f"Invalid name: {name!r}"
assert 0 < age < 150, f"Invalid age: {age}"
assert "@" in email and "." in email, f"Invalid email format: {email}"
# Proceed with user creation...Typprüfungen mit isinstance
Verwende isinstance-Assertions, um Datentypen während der Entwicklung zu prüfen:
def calculate_mean(values):
assert isinstance(values, (list, tuple)), (
f"Expected list or tuple, got {type(values).__name__}"
)
assert all(isinstance(v, (int, float)) for v in values), (
"All values must be numeric"
)
assert len(values) > 0, "Cannot calculate mean of empty sequence"
return sum(values) / len(values)Für Typprüfungen in der Produktion solltest du Python Type Hints zusammen mit einem statischen Typprüfer wie mypy in Betracht ziehen. Assertions dienen dazu, Bugs während der Entwicklung zu finden, nicht zur Laufzeit-Typvalidierung.
Container- und Collection-Prüfungen
# Check dictionary has required keys
required_keys = {"name", "email", "role"}
assert required_keys.issubset(user_data.keys()), (
f"Missing keys: {required_keys - user_data.keys()}"
)
# Check list contains no duplicates
ids = [item.id for item in items]
assert len(ids) == len(set(ids)), (
f"Duplicate IDs found: {[x for x in ids if ids.count(x) > 1]}"
)
# Check that all elements satisfy a condition
scores = [85, 92, 78, 95, 88]
assert all(0 <= s <= 100 for s in scores), (
f"Scores out of range: {[s for s in scores if not 0 <= s <= 100]}"
)Häufige Debugging-Muster mit Assert
Funktions-Preconditions
Preconditions prüfen, ob eine Funktion gültige Eingaben erhält, bevor irgendeine Arbeit erledigt wird. Platziere sie am Anfang der Funktion:
def transfer_money(from_account, to_account, amount):
# Preconditions
assert from_account != to_account, "Cannot transfer to the same account"
assert amount > 0, f"Transfer amount must be positive, got {amount}"
assert from_account.balance >= amount, (
f"Insufficient funds: balance={from_account.balance}, transfer={amount}"
)
from_account.balance -= amount
to_account.balance += amountFunktions-Postconditions
Postconditions prüfen, ob eine Funktion vor der Rückgabe ein korrektes Ergebnis erzeugt hat. Platziere sie direkt vor der return-Anweisung:
def sort_descending(items):
result = sorted(items, reverse=True)
# Postconditions
assert len(result) == len(items), "Sort changed the number of elements"
assert all(result[i] >= result[i+1] for i in range(len(result)-1)), (
"Result is not sorted in descending order"
)
return resultSchleifeninvarianten
Schleifeninvarianten prüfen, ob eine Bedingung in jeder Iteration einer Schleife wahr bleibt. Sie fangen Off-by-one-Fehler, Endlosschleifen und Logikfehler ab:
def binary_search(sorted_list, target):
low = 0
high = len(sorted_list) - 1
while low <= high:
# Loop invariant: target must be in sorted_list[low:high+1] if it exists
assert low >= 0 and high < len(sorted_list), (
f"Bounds out of range: low={low}, high={high}, len={len(sorted_list)}"
)
mid = (low + high) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1Klasseninvarianten
Klasseninvarianten prüfen, ob der interne Zustand eines Objekts nach jeder Operation konsistent bleibt:
class BoundedQueue:
"""A queue with a maximum capacity."""
def __init__(self, capacity):
assert capacity > 0, f"Capacity must be positive, got {capacity}"
self._items = []
self._capacity = capacity
self._check_invariant()
def _check_invariant(self):
assert 0 <= len(self._items) <= self._capacity, (
f"Queue size {len(self._items)} violates capacity {self._capacity}"
)
def enqueue(self, item):
assert len(self._items) < self._capacity, "Queue is full"
self._items.append(item)
self._check_invariant()
def dequeue(self):
assert len(self._items) > 0, "Queue is empty"
item = self._items.pop(0)
self._check_invariant()
return item
def __len__(self):
return len(self._items)Assert vs Raise: Wann man was verwendet
Dies ist einer der wichtigsten Unterschiede im Python-Fehlerhandling. assert und raise dienen grundsätzlich unterschiedlichen Zwecken.
| Feature | assert | raise |
|---|---|---|
| Purpose | Catch programmer errors (bugs) | Handle runtime conditions (expected failures) |
| Can be disabled | Yes, with -O flag | No, always active |
| Use for input validation | Never | Yes |
| Use for external data | Never | Yes |
| Typical exception | AssertionError | ValueError, TypeError, RuntimeError, etc. |
| When it fires | Something is wrong with the code | Something is wrong with the input/environment |
| Audience | The developer | The user or calling code |
| Presence in production | Should not be relied upon | Required |
Verwende assert für: interne Invarianten und Annahmen des Entwicklers
def _calculate_tax(income, brackets):
# Developer assumption: brackets are sorted
assert all(
brackets[i][0] <= brackets[i+1][0]
for i in range(len(brackets) - 1)
), "Tax brackets must be sorted by threshold"
# This is a bug in the code if brackets aren't sorted,
# not a user input problem
...Verwende raise für: Eingabevalidierung und erwartete Fehlerfälle
def create_account(username, password):
if not username or len(username) < 3:
raise ValueError("Username must be at least 3 characters")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters")
# These are user input problems, not programmer bugs.
# They must ALWAYS be checked, even in production.
...Der entscheidende Unterschied: Wenn jemand python -O your_script.py ausführt, wird jede assert-Anweisung vollständig entfernt. Wenn du assert für Eingabevalidierung verwendest, verschwindet deine Validierung im optimierten Modus. Das ist kein theoretisches Risiko -- viele Deployment-Tools und Produktionsumgebungen verwenden das -O-Flag. Einen tieferen Einblick in Exception-Handling-Muster findest du im Python try/except guide.
Die Faustregel
Frag dich: "Wenn diese Prüfung vollständig entfernt würde, könnte ein Benutzer ein Sicherheitsproblem oder eine Datenkorruption verursachen?" Wenn ja, verwende raise. Wenn die Prüfung nur Entwicklerfehler (Bugs im Code selbst) abfängt, verwende assert.
Assert beim Testen
Assertions sind das Rückgrat des Testens in Python. Sowohl unittest als auch pytest verlassen sich auf Assertions, um erwartetes Verhalten zu prüfen.
pytest Assertions
pytest verwendet normale assert-Anweisungen anstelle spezieller Assertion-Methoden. Das ist einer seiner größten Vorteile -- du schreibst natürliches Python statt Methodennamen auswendig zu lernen:
# test_math.py
def test_addition():
assert 2 + 2 == 4
def test_string_methods():
greeting = "hello world"
assert greeting.upper() == "HELLO WORLD"
assert greeting.split() == ["hello", "world"]
def test_list_operations():
items = [1, 2, 3]
items.append(4)
assert len(items) == 4
assert items[-1] == 4pytest Assert Rewriting
Was pytest besonders macht, ist assert rewriting. Wenn eine normale assert fehlschlägt, sagt Python nur AssertionError. pytest schreibt deine assert-Anweisungen beim Import um, um aussagekräftige Fehlermeldungen zu liefern:
def test_comparison():
result = {"name": "Alice", "age": 30}
expected = {"name": "Alice", "age": 31}
assert result == expectedpytest-Ausgabe:
FAILED test_example.py::test_comparison - AssertionError: assert {'age': 30, 'name': 'Alice'} == {'age': 31, 'name': 'Alice'}
Differing items:
{'age': 30} != {'age': 31}Ohne das Rewriting von pytest würdest du nur AssertionError ohne Details sehen. Diese Magie funktioniert, weil pytest einen Import-Hook verwendet, um assert-Anweisungen in ausführlichere Prüfungen umzuwandeln, die Zwischenwerte erfassen.
Häufige pytest-Assertion-Muster
# Check that an exception is raised
import pytest
def test_division_by_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_invalid_input():
with pytest.raises(ValueError, match="must be positive"):
create_user(age=-5)
# Check approximate equality (for floats)
def test_float_calculation():
result = 0.1 + 0.2
assert result == pytest.approx(0.3)
# Check that a value is in a collection
def test_membership():
valid_statuses = {"active", "inactive", "pending"}
user_status = get_user_status(user_id=42)
assert user_status in valid_statuses
# Check with custom message
def test_data_integrity():
records = load_records()
assert len(records) > 0, "No records loaded -- check database connection"Assertions in unittest
Das unittest-Modul bietet methodenbasierte Assertions statt normaler assert-Anweisungen. Diese liefern bessere Fehlermeldungen, ohne dass das Rewriting von pytest nötig ist:
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual("hello".upper(), "HELLO")
def test_contains(self):
self.assertIn("world", "hello world")
def test_raises(self):
with self.assertRaises(TypeError):
"hello" + 5Beide Ansätze sind gültig. pytests normales assert ist lesbarer und Python-typischer. Die methodenbasierten Assertions von unittest liefern detaillierte Meldungen ohne Import-Zeit-Rewriting.
Assertions interaktiv testen
Beim Entwickeln und Debuggen von Test-Assertions können Tools wie RunCell (opens in a new tab) dir erlauben, einzelne Testzellen in Jupyter-Notebooks mit sofortigem Feedback auszuführen. Das ist besonders hilfreich, wenn du komplexe Assertion-Bedingungen schrittweise aufbaust -- du kannst jede Assertion isoliert testen, bevor du sie zu einer vollständigen Test-Suite kombinierst.
Wann man assert NICHT verwenden sollte
Dieser Abschnitt ist kritisch. Der falsche Einsatz von assert erzeugt subtile, gefährliche Bugs, die erst in Produktion auftauchen.
Verwende assert niemals für Eingabevalidierung
# WRONG: This check disappears with python -O
def withdraw(amount):
assert amount > 0, "Amount must be positive"
self.balance -= amount
# RIGHT: This check always runs
def withdraw(amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance -= amountVerwende assert niemals für Daten aus externen Quellen
Daten von Benutzern, Dateien, Netzwerken, Datenbanken oder APIs können fehlerhaft sein. Diese Prüfungen müssen immer ausgeführt werden:
# WRONG: Network data validation with assert
def handle_api_response(response):
assert response.status_code == 200
data = response.json()
assert "results" in data
# RIGHT: Proper error handling for external data
def handle_api_response(response):
if response.status_code != 200:
raise RuntimeError(f"API returned status {response.status_code}")
data = response.json()
if "results" not in data:
raise ValueError("API response missing 'results' field")Verwende assert niemals für Sicherheitsprüfungen
# CATASTROPHICALLY WRONG: Security check with assert
def delete_user(requesting_user, target_user_id):
assert requesting_user.is_admin, "Only admins can delete users"
database.delete(target_user_id)
# RIGHT: Security check that cannot be disabled
def delete_user(requesting_user, target_user_id):
if not requesting_user.is_admin:
raise PermissionError("Only admins can delete users")
database.delete(target_user_id)Mit python -O erlaubt die assert-Version jedem Benutzer, jeden anderen Benutzer zu löschen. Das ist eine echte Sicherheitslücke.
Verwende assert niemals mit Seiteneffekten
Da Assertions deaktiviert werden können, sollte der Ausdruck darin niemals Seiteneffekte haben:
# WRONG: The pop() is a side effect that disappears with -O
assert items.pop() == expected_value
# RIGHT: Separate the side effect from the assertion
value = items.pop()
assert value == expected_valueDas -O-Flag: Wie Assertions verschwinden
Python hat zwei Optimierungsstufen, die Assertions beeinflussen:
python script.py # Normal: __debug__ is True, assertions active
python -O script.py # Optimize: __debug__ is False, assertions removed
python -OO script.py # Extra optimize: assertions removed + docstrings removedWenn Python mit -O läuft, setzt der Interpreter __debug__ auf False und entfernt alle assert-Anweisungen vollständig aus dem Bytecode. Sie werden nicht nur übersprungen -- sie existieren nicht mehr. Die Bedingung wird nie ausgewertet, und die Fehlermeldung wird nie erzeugt.
Das kannst du überprüfen:
# check_debug.py
print(f"__debug__ = {__debug__}")
if __debug__:
print("Assertions are ACTIVE")
else:
print("Assertions are DISABLED")
assert False, "This should raise an error"$ python check_debug.py
__debug__ = True
Assertions are ACTIVE
Traceback (most recent call last):
File "check_debug.py", line 8
AssertionError: This should raise an error
$ python -O check_debug.py
__debug__ = False
Assertions are DISABLED
# No error! The assert was completely removed.Wo -O in der Praxis verwendet wird
- Docker images: Viele Produktions-Dockerfiles verwenden
PYTHONOPTIMIZE=1oderpython -O - Deployment tools: Einige WSGI-Server führen Python im optimierten Modus aus
- Performance-sensitive applications: Das Entfernen von Assertions kann enge Schleifen beschleunigen
- Library code: Bibliotheken sollten niemals davon ausgehen, dass Assertions aktiv sind, weil Nutzer die Optimierungsstufe kontrollieren
Konsequenzen für deinen Code
Betrachte Assertions als Gerüst während des Baus. Sie stützen die Struktur, während du sie aufbaust, aber sie werden entfernt, wenn das Gebäude fertig ist. Dein Code muss korrekt sein, egal ob Assertions vorhanden sind oder nicht.
# This code works correctly with or without assertions:
def safe_divide(a, b):
assert isinstance(a, (int, float)), f"Expected number, got {type(a)}"
assert isinstance(b, (int, float)), f"Expected number, got {type(b)}"
if b == 0:
raise ValueError("Division by zero")
return a / bDie Assertions helfen, Bugs während der Entwicklung zu finden. Das raise behandelt den erwarteten Fehlerfall in der Produktion. Beide Ebenen dienen unterschiedlichen Zwecken.
Benutzerdefinierte Assertion-Helper
Wenn du denselben Assertion-Muster immer wieder schreibst, lagere ihn in eine wiederverwendbare Hilfsfunktion aus.
Einfache Assertion-Funktionen
def assert_positive(value, name="value"):
"""Assert that a value is a positive number."""
assert isinstance(value, (int, float)), (
f"{name} must be a number, got {type(value).__name__}"
)
assert value > 0, f"{name} must be positive, got {value}"
def assert_valid_probability(p, name="probability"):
"""Assert that a value is a valid probability (0 to 1)."""
assert isinstance(p, (int, float)), (
f"{name} must be a number, got {type(p).__name__}"
)
assert 0 <= p <= 1, f"{name} must be between 0 and 1, got {p}"
def assert_same_length(*sequences, names=None):
"""Assert that all sequences have the same length."""
lengths = [len(s) for s in sequences]
if names:
details = ", ".join(f"{n}={l}" for n, l in zip(names, lengths))
else:
details = ", ".join(str(l) for l in lengths)
assert len(set(lengths)) == 1, (
f"Length mismatch: {details}"
)
# Usage
def calculate_weighted_average(values, weights):
assert_same_length(values, weights, names=["values", "weights"])
assert_valid_probability(sum(weights) / len(weights), "average weight")
return sum(v * w for v, w in zip(values, weights)) / sum(weights)Decorator-basierte Assertions
Du kannst Decorators verwenden, um Pre-/Post-Condition-Prüfungen zu Funktionen hinzuzufügen, ohne den Funktionskörper zu überladen:
import functools
def preconditions(**checks):
"""Decorator that asserts preconditions on function arguments."""
def decorator(func):
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name, check_func in checks.items():
value = bound.arguments[param_name]
assert check_func(value), (
f"Precondition failed for '{param_name}': "
f"got {value!r}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@preconditions(
x=lambda v: isinstance(v, (int, float)) and v >= 0,
n=lambda v: isinstance(v, int) and v > 0
)
def nth_root(x, n):
"""Calculate the nth root of x."""
return x ** (1 / n)
# This passes
print(nth_root(27, 3)) # 3.0
# This fails with a clear message
print(nth_root(-1, 2)) # AssertionError: Precondition failed for 'x': got -1Kontextmanager für Assertion-Gruppen
Wenn du mehrere zusammenhängende Bedingungen prüfen und alle Fehler auf einmal melden musst:
class AssertionGroup:
"""Collect multiple assertion failures and report them together."""
def __init__(self, description=""):
self.description = description
self.failures = []
def check(self, condition, message):
if not condition:
self.failures.append(message)
def verify(self):
if self.failures:
header = f"{self.description}: " if self.description else ""
details = "\n - ".join(self.failures)
assert False, f"{header}{len(self.failures)} checks failed:\n - {details}"
# Usage
def validate_user_record(record):
checks = AssertionGroup("User record validation")
checks.check("name" in record, "Missing 'name' field")
checks.check("email" in record, "Missing 'email' field")
checks.check(
record.get("age", 0) > 0,
f"Invalid age: {record.get('age')}"
)
checks.check(
"@" in record.get("email", ""),
f"Invalid email: {record.get('email')}"
)
checks.verify() # Raises with all failures at onceBehandlung von AssertionError mit Try/Except
Du kannst AssertionError wie jede andere Exception abfangen, obwohl das in Anwendungs-Code selten eine gute Idee ist:
try:
assert len(data) > 0, "Data is empty"
process(data)
except AssertionError as e:
print(f"Assertion failed: {e}")
# Handle the failure...Wann das Abfangen von AssertionError sinnvoll ist
Es gibt einige legitime Anwendungsfälle:
1. Test-Frameworks: pytest und unittest fangen AssertionError ab, um Testfehler zu melden, statt abzustürzen.
2. Protokollieren von Assertion-Fehlern in lang laufenden Prozessen:
import logging
logger = logging.getLogger(__name__)
def process_records(records):
failed = []
for record in records:
try:
assert_valid_record(record)
process(record)
except AssertionError as e:
logger.error(f"Skipping invalid record: {e}")
failed.append(record)
if failed:
logger.warning(f"{len(failed)} records failed validation")
return failedFür Produktions-Logging-Muster sollte das Abfangen von AssertionError mit einer ordentlichen Exception-Behandlung kombiniert werden, um sicherzustellen, dass Fehler sichtbar bleiben, ohne den gesamten Prozess abzubrechen.
3. Sanfte Degradierung in nicht kritischen Pfaden:
def generate_report(data):
report = {"data": data, "charts": []}
try:
assert len(data) >= 10, "Not enough data for chart"
chart = create_chart(data)
report["charts"].append(chart)
except AssertionError:
report["charts_note"] = "Insufficient data for visualization"
return reportWann man AssertionError NICHT fangen sollte
Fange AssertionError nicht ab, um Bugs stillschweigend zu unterdrücken. Genau dafür sind Assertions da: Bugs laut und sichtbar zu machen:
# WRONG: Silencing assertions defeats their purpose
try:
assert user.is_valid()
except AssertionError:
pass # Who cares?Praxisbeispiele
Validierung in Datenpipelines
Assertions sind in Datenverarbeitungspipelines äußerst wertvoll, wenn Transformationen bestimmte Eigenschaften erhalten müssen:
import pandas as pd
def clean_sales_data(df):
"""Clean and validate sales data."""
assert isinstance(df, pd.DataFrame), f"Expected DataFrame, got {type(df)}"
assert len(df) > 0, "DataFrame is empty"
initial_rows = len(df)
# Remove duplicates
df = df.drop_duplicates(subset=["order_id"])
assert len(df) > 0, "All rows were duplicates"
# Validate required columns
required = {"order_id", "product", "quantity", "price"}
assert required.issubset(df.columns), (
f"Missing columns: {required - set(df.columns)}"
)
# Clean numeric columns
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["price"] = pd.to_numeric(df["price"], errors="coerce")
# Drop rows with invalid numbers
df = df.dropna(subset=["quantity", "price"])
# Postcondition: all prices and quantities are positive
assert (df["price"] > 0).all(), (
f"Found {(df['price'] <= 0).sum()} non-positive prices"
)
assert (df["quantity"] > 0).all(), (
f"Found {(df['quantity'] <= 0).sum()} non-positive quantities"
)
# Calculate total
df["total"] = df["quantity"] * df["price"]
assert (df["total"] > 0).all(), "Totals must be positive"
print(f"Cleaned {initial_rows} -> {len(df)} rows")
return dfBei der Arbeit mit DataFrames in Data-Science-Workflows ermöglicht dir PyGWalker (opens in a new tab), validierte DataFrames in interaktive Visualisierungen für weitere Analysen umzuwandeln -- ein natürlicher nächster Schritt, nachdem deine Pipeline-Assertions bestätigt haben, dass die Daten sauber und bereit für die Analyse sind.
Überprüfung von API-Antworten
import requests
def fetch_user_profile(user_id):
"""Fetch user profile from API with defensive assertions."""
response = requests.get(f"https://api.example.com/users/{user_id}")
# Use raise for external data validation (not assert!)
if response.status_code != 200:
raise RuntimeError(f"API error: {response.status_code}")
data = response.json()
if "user" not in data:
raise ValueError("API response missing 'user' field")
user = data["user"]
# Use assert for internal invariants -- things that should
# always be true if the API contract is correct
assert "id" in user, "API contract violation: user missing 'id'"
assert user["id"] == user_id, (
f"API returned wrong user: requested {user_id}, got {user['id']}"
)
return userBeachte den Unterschied: raise behandelt erwartete Fehlerfälle (Netzwerkprobleme, fehlerhafte Statuscodes). assert fängt Dinge ab, die auf einen Bug in der API oder in deinen Annahmen über die API hinweisen.
Plausibilitätsprüfungen für Machine-Learning-Modelle
import numpy as np
def train_model(X_train, y_train, X_test, y_test):
"""Train a model with sanity checks at each stage."""
# Data shape assertions
assert X_train.ndim == 2, f"X_train must be 2D, got {X_train.ndim}D"
assert y_train.ndim == 1, f"y_train must be 1D, got {y_train.ndim}D"
assert X_train.shape[0] == y_train.shape[0], (
f"Sample count mismatch: X={X_train.shape[0]}, y={y_train.shape[0]}"
)
assert X_train.shape[1] == X_test.shape[1], (
f"Feature count mismatch: train={X_train.shape[1]}, test={X_test.shape[1]}"
)
# No NaN or Inf in data
assert not np.isnan(X_train).any(), "X_train contains NaN values"
assert not np.isinf(X_train).any(), "X_train contains Inf values"
# Labels are valid
unique_labels = np.unique(y_train)
assert len(unique_labels) >= 2, (
f"Need at least 2 classes, got {len(unique_labels)}"
)
# Train the model
model = fit(X_train, y_train)
# Predictions sanity check
predictions = model.predict(X_test)
assert predictions.shape == y_test.shape, (
f"Prediction shape {predictions.shape} != target shape {y_test.shape}"
)
assert set(predictions).issubset(set(unique_labels)), (
f"Model predicted unknown labels: {set(predictions) - set(unique_labels)}"
)
# Accuracy sanity check (should be better than random)
accuracy = np.mean(predictions == y_test)
random_baseline = 1 / len(unique_labels)
assert accuracy > random_baseline * 0.8, (
f"Accuracy {accuracy:.2%} is worse than random ({random_baseline:.2%})"
)
return modelZustandsmaschinen-Übergänge
class OrderStateMachine:
VALID_TRANSITIONS = {
"created": {"confirmed", "cancelled"},
"confirmed": {"shipped", "cancelled"},
"shipped": {"delivered", "returned"},
"delivered": {"returned"},
"cancelled": set(),
"returned": set(),
}
def __init__(self):
self.state = "created"
self.history = ["created"]
def transition(self, new_state):
assert new_state in self.VALID_TRANSITIONS.get(self.state, set()), (
f"Invalid transition: {self.state} -> {new_state}. "
f"Valid transitions: {self.VALID_TRANSITIONS[self.state]}"
)
self.state = new_state
self.history.append(new_state)
# Invariant: history should always start with "created"
assert self.history[0] == "created", "History corrupted"
# Invariant: current state should match last history entry
assert self.state == self.history[-1], "State/history mismatch"Performance-Aspekte
Wie teuer sind Assertions?
Assertions haben einen kleinen, aber messbaren Aufwand. Der Bedingungsausdruck wird bei jeder Ausführung ausgewertet. Für einfache Prüfungen wie assert x > 0 ist das vernachlässigbar. Für teure Prüfungen kann der Aufwand zunehmen:
import time
data = list(range(1_000_000))
# Fast assertion: O(1)
start = time.perf_counter()
for _ in range(10_000):
assert len(data) > 0
fast_time = time.perf_counter() - start
print(f"Simple assertion: {fast_time:.4f}s")
# Slow assertion: O(n) -- checks every element
start = time.perf_counter()
for _ in range(100):
assert all(isinstance(x, int) for x in data)
slow_time = time.perf_counter() - start
print(f"Expensive assertion: {slow_time:.4f}s")Strategien für teure Assertions
Wenn eine Assertion für eine enge Schleife zu langsam ist, hast du mehrere Möglichkeiten:
1. Prüfe eine Stichprobe statt des gesamten Datensatzes:
import random
def process_large_dataset(records):
# Check a random sample instead of all records
sample = random.sample(records, min(100, len(records)))
assert all(is_valid(r) for r in sample), "Invalid records found in sample"
# Process all records...2. Verwende das __debug__-Flag für bedingte teure Prüfungen:
def matrix_multiply(a, b):
if __debug__:
# This entire block is removed with python -O
assert a.shape[1] == b.shape[0], (
f"Incompatible shapes: {a.shape} x {b.shape}"
)
# Expensive but helpful during development
assert not np.isnan(a).any(), "Matrix a contains NaN"
assert not np.isnan(b).any(), "Matrix b contains NaN"
return a @ b3. Prüfe nur an den Grenzen, nicht in inneren Schleifen:
def process_batch(items):
# Assert once at the boundary
assert all(item.is_valid() for item in items), "Invalid items in batch"
# Inner loop without assertions for performance
results = []
for item in items:
# No assertions here -- we validated above
result = transform(item)
results.append(result)
# Assert once at the output boundary
assert len(results) == len(items), "Result count mismatch"
return resultsBest Practices im Überblick
Hier sind die wichtigsten Prinzipien für den effektiven Einsatz von Assertions:
1. Füge immer eine Meldung hinzu. assert x > 0 sagt dir bei einem Fehler nichts. assert x > 0, f"Expected positive value, got {x}" sagt dir alles.
2. Verwende assert niemals für Eingabevalidierung. Benutzereingaben, Dateiinhalte, API-Antworten und Datenbankabfragen können alle fehlerhaft sein. Prüfe sie mit if/raise.
3. Verwende assert für interne Invarianten. Dinge, die immer wahr sein sollten, wenn dein Code korrekt ist: Funktions-Preconditions, Postconditions, Schleifeninvarianten, Klasseninvarianten.
4. Packe niemals Seiteneffekte in assert. Der Ausdruck assert items.pop() == expected entfernt ein Element aus der Liste -- aber nur, wenn Assertions aktiviert sind.
5. Verwende assert großzügig während der Entwicklung. Sie kosten nichts, wenn sie mit -O deaktiviert sind, und sparen Stunden beim Debugging, wenn sie aktiv sind.
6. Formuliere Assertion-Meldungen handlungsorientiert. Füge den tatsächlichen Wert, den erwarteten Wert und genug Kontext hinzu, um das Problem zu verstehen.
7. Teste deine Assertions. Schreibe Tests, die verifizieren, dass deine Assertions die Bugs erkennen, die sie erkennen sollen.
import pytest
def test_transfer_rejects_negative_amount():
with pytest.raises(AssertionError, match="positive"):
transfer_money(account_a, account_b, amount=-100)FAQ
Fazit
Die Python-assert-Anweisung ist ein leichtgewichtiges, leistungsfähiges Werkzeug für defensive Programmierung. Sie macht implizite Annahmen zu expliziten, erzwungenen Prüfungen, die Bugs genau an der Fehlerstelle abfangen, statt schlechte Daten durch deinen Code wandern zu lassen. Richtig eingesetzt machen Assertions das Debugging schneller, den Code lesbarer und Invarianten selbst dokumentierend.
Die wichtigsten Regeln sind einfach: Verwende assert für interne Invarianten und Annahmen des Entwicklers, verwende raise für Eingabevalidierung und erwartete Fehlerfälle, füge immer beschreibende Meldungen hinzu und packe niemals Seiteneffekte in assert-Ausdrücke. Beim Testen verlassen sich sowohl pytest als auch unittest stark auf Assertions, um erwartetes Verhalten zu prüfen.
Für Data-Science- und Analyse-Workflows passen Assertions natürlich zu Tools wie PyGWalker (opens in a new tab), um DataFrames vor der Visualisierung zu validieren, und zu interaktiven Umgebungen wie RunCell (opens in a new tab), um assertion-geschützte Datenpipelines in Jupyter-Notebooks iterativ aufzubauen und zu testen.
Beherrschst du diese Muster, werden deine Debugging-Sitzungen kürzer, dein Code robuster und deine Tests ausdrucksstärker.
Verwandte Anleitungen
- Python Try/Except -- Ausnahmebehandlung für Laufzeitfehler
- Python Type Hints -- Statische Typprüfung als Ergänzung zu Laufzeit-Assertions
- Python Decorators -- Wiederverwendbare Funktionshüllen einschließlich Assertion-Decorators
- Python unittest -- Integriertes Testframework mit Assertion-Methoden
- Python Logging -- Strukturiertes Logging als Alternative zu
print-Debugging