Python Assert : déboguer plus intelligemment
Mis à jour le
Vous déboguez une fonction de traitement de données. Quelque part entre la lecture du CSV et l’écriture de la sortie, une colonne d’entiers s’est transformée en flottants, une liste qui ne devrait jamais être vide est devenue vide, et un identifiant utilisateur qui devrait être positif vaut maintenant -1. Vous parsemez votre code de print(), relancez le script, plissez les yeux devant la sortie, puis recommencez. Une heure plus tard, vous trouvez le bug -- une seule fonction acceptait silencieusement des entrées invalides et propageait des données corrompues en aval. L’échec réel s’est produit cinquante lignes avant le crash, et rien ne vous l’a signalé.
C’est un problème universel en développement Python. Les bugs se propagent silencieusement. Une valeur None se glisse à travers trois appels de fonctions avant de provoquer un AttributeError. Un index de tableau négatif revient au mauvais élément. Un dictionnaire censé contenir cinq clés n’en a que quatre, et la clé manquante entraîne une erreur logique subtile qui n’apparaît qu’en production. Au moment où l’erreur devient visible, vous avez perdu tout le contexte sur l’endroit où les choses ont réellement mal tourné.
L’instruction assert de Python résout ce problème en détectant les bugs exactement au point de défaillance, avec un message clair sur ce qui n’allait pas. Au lieu d’espérer qu’une mauvaise donnée provoquera un crash évident plus tard, vous déclarez explicitement vos hypothèses -- et Python les vérifie immédiatement.
Qu’est-ce que l’instruction Assert ?
L’instruction assert teste une condition. Si la condition est True, rien ne se passe. Si la condition est False, Python lève immédiatement une AssertionError.
assert 2 + 2 == 4 # Réussit silencieusement
assert 2 + 2 == 5 # Lève AssertionErrorLa syntaxe de base est :
assert condition
assert condition, "Message d'erreur expliquant ce qui s'est mal passé"En interne, Python traduit assert en une instruction if. Voici l’équivalent exact :
# assert condition, message
# est équivalent à :
if __debug__:
if not condition:
raise AssertionError(message)La variable __debug__ est True par défaut. Elle devient False uniquement lorsque Python s’exécute avec l’option -O (optimisation). Cela signifie que les assertions peuvent être complètement désactivées en production -- une caractéristique aux implications importantes que nous verrons plus loin.
Voici ce qui se passe lorsqu’une assertion échoue :
x = -1
assert x >= 0, f"Expected non-negative value, got {x}"Sortie :
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 -1La traceback pointe vers la ligne exacte où l’hypothèse a été violée, et le message indique précisément ce qui n’allait pas. Comparez cela à un mystérieux IndexError vingt lignes plus tard, parce que des valeurs négatives ont continué leur chemin sans contrôle.
Utilisation de base de Assert
Assertions simples
Les assertions les plus simples vérifient une seule condition :
# Vérifier qu'une variable n'est pas None
config = load_config()
assert config is not None
# Vérifier qu'une liste n'est pas vide
items = get_items()
assert len(items) > 0
# Vérifier une propriété mathématique
result = calculate_discount(price=100, percent=20)
assert result == 80Assertions avec messages personnalisés
Incluez toujours un message. Sans message, une assertion échouée donne très peu de contexte :
# Mauvais : pas de message
assert len(users) > 0
# Bien : message descriptif
assert len(users) > 0, "User list is empty -- database query may have failed"
# Bien : inclure la valeur réelle
assert temperature >= -273.15, f"Temperature {temperature}C is below absolute zero"Le message est le deuxième argument de assert, séparé par une virgule. Il peut être n’importe quelle expression produisant une chaîne, y compris des f-strings avec des valeurs à l’exécution :
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)}"
)
# Traiter le lot...Assertions avec parenthèses -- Un piège courant
Il existe un bug subtil qui piège beaucoup de développeurs Python :
# ATTENTION : cette assertion ne peut JAMAIS échouer !
assert(condition, "error message")Cela crée un tuple (condition, "error message"). Un tuple non vide est toujours véridique, donc l’assertion passe toujours. Python vous avertira même :
SyntaxWarning: assertion is always true, perhaps remove parentheses?La forme correcte est :
# Correct : pas de parenthèses
assert condition, "error message"
# Correct aussi : parenthèses uniquement autour de la condition
assert (condition), "error message"
# Correct aussi : sur plusieurs lignes avec continuation implicite
assert (
very_long_condition_that_needs_wrapping
), "error message"Assert avec des conditions complexes
Conditions multiples
Vous pouvez combiner des conditions avec and, or et not :
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}"
# Continuer la création de l'utilisateur...Vérifications de type avec isinstance
Utilisez des assertions isinstance pour vérifier les types de données pendant le développement :
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)Pour la vérification de type en production, envisagez d’utiliser les annotations de type Python avec un vérificateur statique comme mypy. Les assertions servent à détecter des bugs pendant le développement, pas à imposer les types à l’exécution.
Vérifications des conteneurs et collections
# Vérifier qu'un dictionnaire contient les clés requises
required_keys = {"name", "email", "role"}
assert required_keys.issubset(user_data.keys()), (
f"Missing keys: {required_keys - user_data.keys()}"
)
# Vérifier qu'une liste ne contient pas de doublons
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]}"
)
# Vérifier que tous les éléments satisfont une 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]}"
)Modèles courants de débogage avec Assert
Préconditions de fonction
Les préconditions vérifient qu’une fonction reçoit une entrée valide avant d’effectuer le moindre travail. Placez-les en haut de la fonction :
def transfer_money(from_account, to_account, amount):
# Préconditions
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 += amountPostconditions de fonction
Les postconditions vérifient qu’une fonction a produit une sortie correcte avant de retourner. Placez-les juste avant l’instruction return :
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 resultInvariants de boucle
Les invariants de boucle vérifient qu’une condition reste vraie à chaque itération d’une boucle. Ils détectent les erreurs de décalage d’un cran, les boucles infinies et les bugs logiques :
def binary_search(sorted_list, target):
low = 0
high = len(sorted_list) - 1
while low <= high:
# Invariant de boucle : target doit être dans sorted_list[low:high+1] s'il existe
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 -1Invariants de classe
Les invariants de classe vérifient que l’état interne d’un objet reste cohérent après chaque opération :
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 : quand utiliser l’un ou l’autre
C’est l’une des distinctions les plus importantes dans la gestion des erreurs en Python. assert et raise servent des objectifs fondamentalement différents.
| Fonctionnalité | assert | raise |
|---|---|---|
| Objectif | Détecter des erreurs de programmeur (bugs) | Gérer des conditions d’exécution (échecs attendus) |
| Peut être désactivé | Oui, avec l’option -O | Non, toujours actif |
| À utiliser pour la validation d’entrée | Jamais | Oui |
| À utiliser pour des données externes | Jamais | Oui |
| Exception typique | AssertionError | ValueError, TypeError, RuntimeError, etc. |
| Quand cela se déclenche | Quelque chose ne va pas dans le code | Quelque chose ne va pas dans l’entrée/l’environnement |
| Public visé | Le développeur | L’utilisateur ou le code appelant |
| Présence en production | Ne doit pas être utilisée comme garantie | Requise |
Utiliser assert pour : les invariants internes et les hypothèses du développeur
def _calculate_tax(income, brackets):
# Hypothèse du développeur : les brackets sont triés
assert all(
brackets[i][0] <= brackets[i+1][0]
for i in range(len(brackets) - 1)
), "Tax brackets must be sorted by threshold"
# C'est un bug dans le code si les brackets ne sont pas triés,
# pas un problème d'entrée utilisateur
...Utiliser raise pour : la validation d’entrée et les conditions d’erreur attendues
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")
# Ce sont des problèmes d'entrée utilisateur, pas des bugs de programmeur.
# Ils doivent TOUJOURS être vérifiés, même en production.
...La différence essentielle : si quelqu’un exécute python -O your_script.py, chaque instruction assert est complètement supprimée. Si vous utilisez assert pour valider des entrées, votre validation disparaît en mode optimisé. Ce n’est pas un risque théorique -- de nombreux outils de déploiement et environnements de production utilisent l’option -O. Pour une exploration plus approfondie des patterns de gestion d’exceptions, voir le guide Python try/except.
La règle pratique
Demandez-vous : « Si cette vérification était supprimée entièrement, un utilisateur pourrait-il provoquer un problème de sécurité ou une corruption de données ? » Si oui, utilisez raise. Si la vérification ne fait que détecter des erreurs de développement (bugs dans le code lui-même), utilisez assert.
Assert dans les tests
Les assertions sont la base des tests en Python. Tant unittest que pytest s’appuient sur elles pour vérifier le comportement attendu.
Assertions pytest
pytest utilise de simples instructions assert au lieu de méthodes d’assertion spéciales. C’est l’un de ses plus grands avantages -- vous écrivez du Python naturel au lieu de mémoriser des noms de méthodes :
# 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] == 4Réécriture des assert par pytest
Ce qui rend pytest spécial, c’est la réécriture des assert. Lorsqu’un assert simple échoue, Python indique seulement AssertionError. pytest réécrit vos instructions assert au moment de l’import pour fournir des messages d’échec riches :
def test_comparison():
result = {"name": "Alice", "age": 30}
expected = {"name": "Alice", "age": 31}
assert result == expectedSortie pytest :
FAILED test_example.py::test_comparison - AssertionError: assert {'age': 30, 'name': 'Alice'} == {'age': 31, 'name': 'Alice'}
Differing items:
{'age': 30} != {'age': 31}Sans la réécriture de pytest, vous ne verriez qu’une simple AssertionError sans détails. Cette magie fonctionne parce que pytest utilise un hook d’import pour transformer les instructions assert en vérifications plus verbeuses qui capturent les valeurs intermédiaires.
Patterns courants d’assertion pytest
# Vérifier qu'une exception est levée
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)
# Vérifier l'égalité approximative (pour les flottants)
def test_float_calculation():
result = 0.1 + 0.2
assert result == pytest.approx(0.3)
# Vérifier qu'une valeur est dans une collection
def test_membership():
valid_statuses = {"active", "inactive", "pending"}
user_status = get_user_status(user_id=42)
assert user_status in valid_statuses
# Vérifier avec un message personnalisé
def test_data_integrity():
records = load_records()
assert len(records) > 0, "No records loaded -- check database connection"Assertions dans unittest
Le module unittest fournit des assertions basées sur des méthodes au lieu de simples assert. Elles donnent de meilleurs messages d’erreur sans nécessiter la réécriture de pytest :
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" + 5Les deux approches sont valides. Les assert simples de pytest sont plus lisibles et plus idiomatiques en Python. Les assertions basées sur des méthodes de unittest fournissent des messages détaillés sans réécriture à l’import.
Tester les assertions de manière interactive
Lors du développement et du débogage des assertions de test de façon interactive, des outils comme RunCell (opens in a new tab) vous permettent d’exécuter des cellules de test individuelles dans des notebooks Jupyter avec un retour immédiat. C’est particulièrement utile lorsque vous construisez des conditions d’assertion complexes étape par étape -- vous pouvez tester chaque assertion isolément avant de les combiner en une suite de tests complète.
Quand NE PAS utiliser Assert
Cette section est critique. Un mauvais usage de assert crée des bugs subtils et dangereux qui n’apparaissent qu’en production.
N’utilisez jamais assert pour valider des entrées
# FAUX : cette vérification disparaît avec python -O
def withdraw(amount):
assert amount > 0, "Amount must be positive"
self.balance -= amount
# CORRECT : cette vérification s'exécute toujours
def withdraw(amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance -= amountN’utilisez jamais assert pour des données provenant de sources externes
Toute donnée provenant d’utilisateurs, de fichiers, de réseaux, de bases de données ou d’API peut être mal formée. Ces vérifications doivent toujours s’exécuter :
# FAUX : validation de données réseau avec assert
def handle_api_response(response):
assert response.status_code == 200
data = response.json()
assert "results" in data
# CORRECT : gestion correcte des erreurs pour des données externes
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")N’utilisez jamais assert pour des contrôles de sécurité
# CATÉGORIQUEMENT FAUX : contrôle de sécurité avec assert
def delete_user(requesting_user, target_user_id):
assert requesting_user.is_admin, "Only admins can delete users"
database.delete(target_user_id)
# CORRECT : contrôle de sécurité qui ne peut pas être désactivé
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)Avec python -O, la version avec assert permet à n’importe quel utilisateur de supprimer n’importe quel autre utilisateur. C’est une vraie vulnérabilité de sécurité.
N’utilisez jamais assert avec des effets de bord
Comme les assertions peuvent être désactivées, l’expression qu’elles contiennent ne doit jamais avoir d’effets de bord :
# FAUX : le pop() est un effet de bord qui disparaît avec -O
assert items.pop() == expected_value
# CORRECT : séparer l'effet de bord de l'assertion
value = items.pop()
assert value == expected_valueLe drapeau -O : comment les assertions disparaissent
Python dispose de deux niveaux d’optimisation qui affectent les assertions :
python script.py # Normal : __debug__ est True, assertions actives
python -O script.py # Optimisé : __debug__ est False, assertions supprimées
python -OO script.py # Optimisé davantage : assertions supprimées + docstrings suppriméesLorsque Python s’exécute avec -O, l’interpréteur définit __debug__ sur False et supprime complètement toutes les instructions assert du bytecode. Elles ne sont pas simplement ignorées -- elles cessent d’exister. La condition n’est jamais évaluée, et le message d’erreur n’est jamais construit.
Vous pouvez le vérifier :
# 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.Où -O est utilisé en pratique
- Images Docker : de nombreux Dockerfiles de production utilisent
PYTHONOPTIMIZE=1oupython -O - Outils de déploiement : certains serveurs WSGI exécutent Python en mode optimisé
- Applications sensibles aux performances : supprimer les assertions peut accélérer les boucles serrées
- Code de bibliothèque : les bibliothèques ne doivent jamais supposer que les assertions sont actives, car les consommateurs contrôlent le niveau d’optimisation
Implications pour votre code
Considérez les assertions comme un échafaudage pendant la construction. Elles soutiennent la structure pendant que vous bâtissez, mais elles sont retirées une fois le bâtiment terminé. Votre code doit être correct, que les assertions soient présentes ou non.
# Ce code fonctionne correctement avec ou sans 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 / bLes assertions aident à détecter des bugs pendant le développement. Le raise gère la condition d’erreur attendue en production. Les deux couches servent des objectifs différents.
Helpers d’assertion personnalisés
Lorsque vous vous surprenez à écrire encore et encore le même modèle d’assertion, extrayez-le dans une fonction utilitaire réutilisable.
Fonctions d’assertion simples
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}"
)
# Utilisation
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)Assertions basées sur des décorateurs
Vous pouvez utiliser des décorateurs pour ajouter des vérifications de pré/postconditions aux fonctions sans encombrer le corps de la fonction :
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)
# Ceci passe
print(nth_root(27, 3)) # 3.0
# Ceci échoue avec un message clair
print(nth_root(-1, 2)) # AssertionError: Precondition failed for 'x': got -1Gestionnaire de contexte pour des groupes d’assertions
Quand vous devez vérifier plusieurs conditions liées et signaler toutes les défaillances en une seule fois :
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}"
# Utilisation
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() # Lève une erreur avec tous les échecs à la foisGérer AssertionError avec Try/Except
Vous pouvez attraper AssertionError comme n’importe quelle autre exception, bien que ce soit rarement une bonne idée dans le code applicatif :
try:
assert len(data) > 0, "Data is empty"
process(data)
except AssertionError as e:
print(f"Assertion failed: {e}")
# Gérer l'échec...Quand attraper AssertionError a du sens
Il existe quelques cas d’usage légitimes :
1. Les frameworks de test : pytest et unittest attrapent AssertionError pour signaler des échecs de test au lieu de provoquer un crash.
2. Journaliser les échecs d’assertion dans des processus longs :
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 failedPour des patterns de logging en production, attraper AssertionError devrait être combiné à une gestion appropriée des exceptions afin de garantir que les défaillances restent visibles sans faire tomber tout le processus.
3. Dégradation gracieuse dans des chemins non critiques :
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 reportQuand NE PAS attraper AssertionError
N’attrapez pas AssertionError pour masquer silencieusement des bugs. Le but même des assertions est de rendre les bugs bruyants et visibles :
# FAUX : faire taire les assertions va à l'encontre de leur objectif
try:
assert user.is_valid()
except AssertionError:
pass # Et alors ?Exemples concrets
Validation d’un pipeline de données
Les assertions sont précieuses dans les pipelines de traitement de données où les transformations doivent préserver certaines propriétés :
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)
# Supprimer les doublons
df = df.drop_duplicates(subset=["order_id"])
assert len(df) > 0, "All rows were duplicates"
# Valider les colonnes requises
required = {"order_id", "product", "quantity", "price"}
assert required.issubset(df.columns), (
f"Missing columns: {required - set(df.columns)}"
)
# Nettoyer les colonnes numériques
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["price"] = pd.to_numeric(df["price"], errors="coerce")
# Supprimer les lignes contenant des nombres invalides
df = df.dropna(subset=["quantity", "price"])
# Postcondition : tous les prix et quantités sont positifs
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"
)
# Calculer le 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 dfLorsqu’on travaille avec des DataFrames dans des workflows de data science, PyGWalker (opens in a new tab) vous permet de transformer des DataFrames validés en visualisations interactives pour aller plus loin dans l’exploration -- une suite naturelle une fois que vos assertions de pipeline confirment que les données sont propres et prêtes à l’analyse.
Vérification de réponses API
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}")
# Utiliser raise pour valider des données externes (pas 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"]
# Utiliser assert pour les invariants internes -- des choses qui devraient
# toujours être vraies si le contrat de l'API est 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 userRemarquez la distinction : raise gère les conditions d’erreur attendues (problèmes réseau, mauvais codes de statut). assert détecte les choses qui indiquent un bug dans l’API ou dans les hypothèses de votre code sur l’API.
Vérifications de cohérence pour des modèles de machine learning
import numpy as np
def train_model(X_train, y_train, X_test, y_test):
"""Train a model with sanity checks at each stage."""
# Assertions sur les formes des données
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]}"
)
# Pas de NaN ni d'Inf dans les données
assert not np.isnan(X_train).any(), "X_train contains NaN values"
assert not np.isinf(X_train).any(), "X_train contains Inf values"
# Les étiquettes sont valides
unique_labels = np.unique(y_train)
assert len(unique_labels) >= 2, (
f"Need at least 2 classes, got {len(unique_labels)}"
)
# Entraîner le modèle
model = fit(X_train, y_train)
# Vérification de cohérence des prédictions
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)}"
)
# Vérification de cohérence de l'exactitude (devrait être meilleure que le hasard)
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 modelTransitions de machine à états
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 : l'historique doit toujours commencer par "created"
assert self.history[0] == "created", "History corrupted"
# Invariant : l'état courant doit correspondre à la dernière entrée de l'historique
assert self.state == self.history[-1], "State/history mismatch"Considérations de performance
Quel est le coût des assertions ?
Les assertions ont un coût faible mais mesurable. L’expression de la condition est évaluée à chaque exécution de l’assertion. Pour des vérifications simples comme assert x > 0, cela est négligeable. Pour des vérifications coûteuses, le coût peut s’accumuler :
import time
data = list(range(1_000_000))
# Assertion rapide : 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")
# Assertion lente : O(n) -- vérifie chaque élément
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")Stratégies pour les assertions coûteuses
Si une assertion est trop lente pour une boucle serrée, plusieurs options s’offrent à vous :
1. Vérifier un échantillon plutôt que tout le jeu de données :
import random
def process_large_dataset(records):
# Vérifier un échantillon aléatoire plutôt que tous les enregistrements
sample = random.sample(records, min(100, len(records)))
assert all(is_valid(r) for r in sample), "Invalid records found in sample"
# Traiter tous les enregistrements...2. Utiliser le drapeau __debug__ pour des vérifications coûteuses conditionnelles :
def matrix_multiply(a, b):
if __debug__:
# Tout ce bloc est supprimé avec python -O
assert a.shape[1] == b.shape[0], (
f"Incompatible shapes: {a.shape} x {b.shape}"
)
# Coûteux mais utile pendant le développement
assert not np.isnan(a).any(), "Matrix a contains NaN"
assert not np.isnan(b).any(), "Matrix b contains NaN"
return a @ b3. N’asserter qu’aux frontières, pas dans les boucles internes :
def process_batch(items):
# Asserter une fois à la frontière
assert all(item.is_valid() for item in items), "Invalid items in batch"
# Boucle interne sans assertions pour les performances
results = []
for item in items:
# Pas d'assertion ici -- nous avons validé au-dessus
result = transform(item)
results.append(result)
# Asserter une fois à la frontière de sortie
assert len(results) == len(items), "Result count mismatch"
return resultsRésumé des bonnes pratiques
Voici les principes clés pour utiliser efficacement les assertions :
1. Incluez toujours un message. assert x > 0 ne vous dit rien lorsqu’il échoue. assert x > 0, f"Expected positive value, got {x}" vous dit tout.
2. N’utilisez jamais assert pour valider des entrées. Les entrées utilisateur, le contenu des fichiers, les réponses d’API et les requêtes de base de données peuvent tous être mal formés. Validez-les avec if/raise.
3. Utilisez assert pour les invariants internes. Les choses qui devraient toujours être vraies si votre code est correct : préconditions de fonction, postconditions, invariants de boucle, invariants de classe.
4. Ne mettez jamais d’effets de bord dans assert. L’expression assert items.pop() == expected retire un élément de la liste -- mais seulement lorsque les assertions sont activées.
5. Utilisez assert généreusement pendant le développement. Elles ne coûtent rien lorsqu’elles sont désactivées avec -O et vous font gagner des heures de débogage lorsqu’elles sont activées.
6. Rendez les messages d’assertion exploitables. Incluez la valeur réelle, la valeur attendue et suffisamment de contexte pour comprendre ce qui n’allait pas.
7. Testez vos assertions. Écrivez des tests qui vérifient que vos assertions détectent bien les bugs qu’elles sont censées attraper.
import pytest
def test_transfer_rejects_negative_amount():
with pytest.raises(AssertionError, match="positive"):
transfer_money(account_a, account_b, amount=-100)FAQ
Conclusion
L’instruction Python assert est un outil léger et puissant pour la programmation défensive. Elle transforme des hypothèses implicites en vérifications explicites et imposées, qui détectent les bugs au point de défaillance au lieu de laisser les mauvaises données se propager dans votre code. Bien utilisées, les assertions rendent le débogage plus rapide, le code plus lisible et les invariants auto-documentés.
Les règles clés sont simples : utilisez assert pour les invariants internes et les hypothèses du développeur, utilisez raise pour la validation d’entrée et les conditions d’erreur attendues, incluez toujours des messages descriptifs, et ne mettez jamais d’effets de bord dans les expressions assert. Dans les tests, pytest et unittest s’appuient fortement sur les assertions pour vérifier le comportement attendu.
Pour les workflows de data science et d’analyse, les assertions s’associent naturellement à des outils comme PyGWalker (opens in a new tab) pour valider les DataFrames avant visualisation, et à des environnements interactifs comme RunCell (opens in a new tab) pour construire et tester itérativement des pipelines de données protégés par des assertions dans des notebooks Jupyter.
Maîtrisez ces modèles et vos sessions de débogage seront plus courtes, votre code deviendra plus robuste, et vos tests seront plus expressifs.
Guides associés
- Python Try/Except -- Gestion des exceptions pour les erreurs d’exécution
- Python Type Hints -- Vérification statique des types en complément des assertions à l’exécution
- Python Decorators -- Enveloppes de fonctions réutilisables, y compris des décorateurs d’assertion
- Python unittest -- Framework de test intégré avec des méthodes d’assertion
- Python Logging -- Journalisation structurée comme alternative à
printpour le débogage