Python Assert: depura de forma más inteligente
Actualizado el
Estás depurando una función de procesamiento de datos. En algún punto entre leer el CSV y escribir la salida, una columna de enteros se convirtió en flotantes, una lista que nunca debería estar vacía quedó vacía, y un ID de usuario que debería ser positivo ahora es -1. Llenas todo de sentencias print(), vuelves a ejecutar el script, entrecierras los ojos mirando la salida y repites. Una hora después encuentras el error: una sola función aceptaba silenciosamente datos inválidos y propagaba datos corruptos río abajo. El fallo real ocurrió cincuenta líneas antes del crash, y nada te lo avisó.
Este es un problema universal en el desarrollo con Python. Los bugs se propagan silenciosamente. Un valor None se cuela por tres llamadas a funciones antes de causar un AttributeError. Un índice negativo en un array envuelve y apunta al elemento equivocado. Un diccionario que debía tener cinco claves solo tiene cuatro, y la clave faltante provoca un error lógico sutil que solo aparece en producción. Cuando el error se vuelve visible, ya has perdido todo el contexto de dónde ocurrió realmente el problema.
La sentencia assert de Python resuelve esto al detectar bugs en el punto exacto de fallo, con un mensaje claro sobre lo que salió mal. En lugar de esperar que unos datos malos acaben causando un crash evidente, declaras tus supuestos explícitamente — y Python los hace cumplir de inmediato.
¿Qué es la sentencia Assert?
La sentencia assert comprueba una condición. Si la condición es True, no ocurre nada. Si la condición es False, Python lanza inmediatamente una AssertionError.
assert 2 + 2 == 4 # Pasa silenciosamente
assert 2 + 2 == 5 # Lanza AssertionErrorLa sintaxis básica es:
assert condition
assert condition, "Error message explaining what went wrong"Internamente, Python traduce assert a una sentencia if. Este es el equivalente exacto:
# assert condition, message
# es equivalente a:
if __debug__:
if not condition:
raise AssertionError(message)La variable __debug__ es True por defecto. Solo pasa a False cuando Python se ejecuta con la bandera -O (optimize). Esto significa que las aserciones pueden desactivarse por completo en producción — una característica con implicaciones importantes que veremos más adelante.
Esto es lo que ocurre cuando falla una aserción:
x = -1
assert x >= 0, f"Expected non-negative value, got {x}"Salida:
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 -1El traceback apunta a la línea exacta donde se violó el supuesto, y el mensaje te dice exactamente qué salió mal. Compáralo con un IndexError misterioso veinte líneas después, causado por valores negativos que se propagaron sin control.
Uso Básico de Assert
Aserciones simples
Las aserciones más sencillas comprueban una sola condición:
# Comprobar que una variable no sea None
config = load_config()
assert config is not None
# Comprobar que una lista no esté vacía
items = get_items()
assert len(items) > 0
# Comprobar una propiedad matemática
result = calculate_discount(price=100, percent=20)
assert result == 80Aserciones con mensajes personalizados
Incluye siempre un mensaje. Sin él, una aserción fallida aporta casi ningún contexto:
# Malo: sin mensaje
assert len(users) > 0
# Bien: mensaje descriptivo
assert len(users) > 0, "User list is empty -- database query may have failed"
# Bien: incluir el valor real
assert temperature >= -273.15, f"Temperature {temperature}C is below absolute zero"El mensaje es el segundo argumento de assert, separado por una coma. Puede ser cualquier expresión que produzca una cadena, incluidas f-strings con valores en tiempo de ejecución:
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...Aserciones con paréntesis -- un error común
Hay un bug sutil que atrapa a muchos desarrolladores de Python:
# WARNING: This assertion NEVER fails!
assert(condition, "error message")Esto crea una tupla (condition, "error message"). Una tupla no vacía siempre es truthy, así que la aserción siempre pasa. Python incluso te advertirá:
SyntaxWarning: assertion is always true, perhaps remove parentheses?La forma correcta es:
# 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 con Condiciones Complejas
Múltiples condiciones
Puedes combinar condiciones con and, or y 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}"
# Proceed with user creation...Comprobaciones de tipo con isinstance
Usa aserciones con isinstance para verificar tipos de datos durante el desarrollo:
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)Para la comprobación de tipos en producción, considera usar Python type hints con un comprobador estático como mypy. Las aserciones sirven para detectar bugs durante el desarrollo, no para imponer tipos en tiempo de ejecución.
Comprobaciones de contenedores y colecciones
# 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]}"
)Patrones Comunes de Depuración con Assert
Precondiciones de función
Las precondiciones verifican que una función reciba una entrada válida antes de hacer cualquier trabajo. Colócalas al principio de la función:
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 += amountPostcondiciones de función
Las postcondiciones verifican que una función produjo una salida correcta antes de devolverla. Colócalas justo antes de la sentencia 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 resultInvariantes de bucle
Los invariantes de bucle verifican que una condición se mantenga verdadera en cada iteración de un bucle. Detectan errores off-by-one, bucles infinitos y bugs lógicos:
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 -1Invariantes de clase
Los invariantes de clase verifican que el estado interno de un objeto permanezca consistente después de cada operación:
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: Cuándo Usar Cada Uno
Esta es una de las distinciones más importantes en el manejo de errores en Python. assert y raise cumplen propósitos fundamentalmente distintos.
| 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 |
Usa assert para: invariantes internas y supuestos del desarrollador
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
...Usa raise para: validación de entrada y condiciones de error esperadas
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.
...La diferencia crítica: si alguien ejecuta python -O your_script.py, cada sentencia assert se elimina por completo. Si usas assert para validar entradas, esa validación desaparece en modo optimizado. Esto no es un riesgo teórico: muchas herramientas de despliegue y entornos de producción usan la bandera -O. Para una visión más profunda de los patrones de manejo de excepciones, consulta la Python try/except guide.
La regla general
Pregúntate: "Si esta comprobación se eliminara por completo, ¿podría un usuario causar un problema de seguridad o corrupción de datos?" Si la respuesta es sí, usa raise. Si la comprobación solo detecta errores del desarrollador (bugs en el propio código), usa assert.
Assert en las Pruebas
Las aserciones son la base de las pruebas en Python. Tanto unittest como pytest dependen de aserciones para verificar el comportamiento esperado.
Aserciones en pytest
pytest usa sentencias assert normales en lugar de métodos de aserción especiales. Esta es una de sus mayores ventajas: escribes Python natural en vez de memorizar nombres de métodos:
# 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] == 4Reescritura de assert en pytest
Lo que hace especial a pytest es la reescritura de asserts. Cuando falla un assert simple, Python solo dice AssertionError. pytest reescribe tus sentencias assert en tiempo de importación para proporcionar mensajes de error enriquecidos:
def test_comparison():
result = {"name": "Alice", "age": 30}
expected = {"name": "Alice", "age": 31}
assert result == expectedSalida de pytest:
FAILED test_example.py::test_comparison - AssertionError: assert {'age': 30, 'name': 'Alice'} == {'age': 31, 'name': 'Alice'}
Differing items:
{'age': 30} != {'age': 31}Sin la reescritura de pytest, solo verías AssertionError sin detalles. Esta magia funciona porque pytest usa un hook de importación para transformar las sentencias assert en comprobaciones más verbosas que capturan valores intermedios.
Patrones comunes de aserción en pytest
# 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"Aserciones en unittest
El módulo unittest proporciona aserciones basadas en métodos en lugar de assert simple. Estas ofrecen mejores mensajes de error sin necesitar la reescritura 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" + 5Ambos enfoques son válidos. El assert simple de pytest es más legible y más idiomático en Python. Las aserciones basadas en métodos de unittest proporcionan mensajes detallados sin reescritura en tiempo de importación.
Probar aserciones interactivamente
Al desarrollar y depurar aserciones de prueba de forma interactiva, herramientas como RunCell (opens in a new tab) te permiten ejecutar celdas de prueba individuales en notebooks de Jupyter con retroalimentación instantánea. Esto es especialmente útil cuando vas construyendo condiciones de aserción complejas paso a paso: puedes probar cada aserción de forma aislada antes de combinarlas en una suite completa de pruebas.
Cuándo NO Usar Assert
Esta sección es crítica. Usar mal assert crea bugs sutiles y peligrosos que solo aparecen en producción.
Nunca uses assert para validación de entrada
# 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 -= amountNunca uses assert para datos de fuentes externas
Cualquier dato procedente de usuarios, archivos, redes, bases de datos o APIs puede estar mal formado. Estas comprobaciones siempre deben ejecutarse:
# 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")Nunca uses assert para comprobaciones de seguridad
# 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)Con python -O, la versión con assert permite que cualquier usuario borre a cualquier otro usuario. Esta es una vulnerabilidad de seguridad real.
Nunca uses assert con efectos secundarios
Como las aserciones pueden desactivarse, la expresión dentro de ellas nunca debe tener efectos secundarios:
# 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_valueLa bandera -O: cómo desaparecen las aserciones
Python tiene dos niveles de optimización que afectan a las aserciones:
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 removedCuando Python se ejecuta con -O, el intérprete establece __debug__ en False y elimina por completo todas las sentencias assert del bytecode. No solo se omiten: dejan de existir. La condición nunca se evalúa y el mensaje de error nunca se construye.
Puedes comprobarlo:
# 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.Dónde se usa -O en la práctica
- Docker images: Many production Dockerfiles use
PYTHONOPTIMIZE=1orpython -O - Deployment tools: Some WSGI servers run Python in optimized mode
- Performance-sensitive applications: Removing assertions can speed up tight loops
- Library code: Libraries should never assume assertions are active because consumers control the optimization level
Implicaciones para tu código
Piensa en las aserciones como andamios durante la construcción. Sostienen la estructura mientras la construyes, pero se retiran cuando el edificio está terminado. Tu código debe funcionar correctamente tanto si las aserciones están presentes como si no.
# 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 / bLas aserciones ayudan a detectar bugs durante el desarrollo. El raise maneja la condición de error esperada en producción. Ambas capas cumplen funciones distintas.
Funciones Auxiliares Personalizadas para Assert
Cuando te encuentres escribiendo repetidamente el mismo patrón de aserción, extráelo en una función auxiliar reutilizable.
Funciones simples de aserción
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)Aserciones basadas en decoradores
Puedes usar decorators para añadir comprobaciones de pre/postcondición a funciones sin ensuciar el cuerpo de la función:
import functools
def preconditions(**checks):
"""Decorator that asserts preconditions on function arguments."""
def decorator(func):
@functools.wraps(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 -1Gestor de contexto para grupos de aserciones
Cuando necesitas comprobar varias condiciones relacionadas y reportar todos los fallos a la vez:
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 onceManejo de AssertionError con Try/Except
Puedes capturar AssertionError como cualquier otra excepción, aunque rara vez es una buena idea en código de aplicación:
try:
assert len(data) > 0, "Data is empty"
process(data)
except AssertionError as e:
print(f"Assertion failed: {e}")
# Handle the failure...Cuándo tiene sentido capturar AssertionError
Hay algunos casos legítimos:
1. Frameworks de pruebas: pytest y unittest capturan AssertionError para reportar fallos de prueba en lugar de hacer crash.
2. Registrar fallos de aserción en procesos de larga duración:
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 failedPara patrones de logging en producción, capturar AssertionError debería combinarse con un manejo adecuado de excepciones para asegurar que los fallos sean visibles sin hacer caer todo el proceso.
3. Degradación elegante en rutas no críticas:
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 reportCuándo NO capturar AssertionError
No captures AssertionError para suprimir bugs en silencio. El propósito de las aserciones es precisamente hacer que los bugs sean ruidosos y visibles:
# WRONG: Silencing assertions defeats their purpose
try:
assert user.is_valid()
except AssertionError:
pass # Who cares?Ejemplos del Mundo Real
Validación de un pipeline de datos
Las aserciones son invaluables en pipelines de procesamiento de datos donde las transformaciones deben conservar ciertas propiedades:
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 dfCuando trabajas con DataFrames en flujos de ciencia de datos, PyGWalker (opens in a new tab) te permite convertir DataFrames validados en visualizaciones interactivas para seguir explorando: un siguiente paso natural después de que tus aserciones de pipeline confirmen que los datos están limpios y listos para el análisis.
Comprobación de respuestas de 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}")
# 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 userFíjate en la distinción: raise maneja condiciones de error esperadas (problemas de red, códigos de estado incorrectos). assert detecta cosas que indican un bug en la API o en los supuestos de tu código sobre la API.
Comprobaciones de sanidad en modelos 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."""
# 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 modelTransiciones de máquina de estados
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"Consideraciones de Rendimiento
¿Cuánto cuestan las aserciones?
Las aserciones tienen un coste pequeño pero medible. La expresión de la condición se evalúa cada vez que se ejecuta la aserción. Para comprobaciones simples como assert x > 0, esto es despreciable. Para comprobaciones costosas, el coste puede acumularse:
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")Estrategias para aserciones costosas
Si una aserción es demasiado lenta para un bucle muy ajustado, tienes varias opciones:
1. Comprobar una muestra en lugar de todo el conjunto de datos:
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. Usar la bandera __debug__ para comprobaciones costosas condicionales:
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. Aserta solo en los límites, no dentro de los bucles internos:
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 resultsResumen de Buenas Prácticas
Estos son los principios clave para usar aserciones de forma efectiva:
1. Incluye siempre un mensaje. assert x > 0 no te dice nada cuando falla. assert x > 0, f"Expected positive value, got {x}" te lo dice todo.
2. Nunca uses assert para validación de entrada. La entrada del usuario, el contenido de archivos, las respuestas de APIs y las consultas a bases de datos pueden estar mal formadas. Valídalas con if/raise.
3. Usa assert para invariantes internas. Cosas que siempre deberían ser ciertas si tu código es correcto: precondiciones de función, postcondiciones, invariantes de bucle, invariantes de clase.
4. Nunca pongas efectos secundarios dentro de assert. La expresión assert items.pop() == expected elimina un elemento de la lista — pero solo cuando las aserciones están activas.
5. Usa assert libremente durante el desarrollo. No cuestan nada cuando se desactivan con -O y ahorran horas de depuración cuando están activas.
6. Mantén los mensajes de aserción accionables. Incluye el valor real, el valor esperado y suficiente contexto para entender qué salió mal.
7. Prueba tus aserciones. Escribe tests que verifiquen que tus aserciones detectan los bugs que deben detectar.
import pytest
def test_transfer_rejects_negative_amount():
with pytest.raises(AssertionError, match="positive"):
transfer_money(account_a, account_b, amount=-100)FAQ
Conclusión
La sentencia assert de Python es una herramienta ligera y potente para la programación defensiva. Convierte supuestos implícitos en comprobaciones explícitas y exigibles que detectan bugs en el punto exacto de fallo en lugar de dejar que los datos malos se propaguen por tu código. Usadas correctamente, las aserciones hacen la depuración más rápida, el código más legible y los invariantes se documentan por sí mismos.
Las reglas clave son sencillas: usa assert para invariantes internas y supuestos del desarrollador, usa raise para validación de entrada y condiciones de error esperadas, incluye siempre mensajes descriptivos y nunca pongas efectos secundarios dentro de expresiones assert. En las pruebas, tanto pytest como unittest dependen en gran medida de las aserciones para verificar el comportamiento esperado.
Para flujos de trabajo de ciencia de datos y análisis, las aserciones encajan de forma natural con herramientas como PyGWalker (opens in a new tab) para validar DataFrames antes de la visualización, y entornos interactivos como RunCell (opens in a new tab) para construir y probar iterativamente pipelines de datos protegidos por aserciones en notebooks de Jupyter.
Domina estos patrones y tus sesiones de depuración se acortarán, tu código será más robusto y tus pruebas serán más expresivas.
Guías Relacionadas
- Python Try/Except -- Manejo de excepciones para errores en tiempo de ejecución
- Python Type Hints -- Comprobación estática de tipos como complemento de las aserciones en tiempo de ejecución
- Python Decorators -- Wrappers reutilizables de funciones, incluidos decoradores de aserción
- Python unittest -- Framework de pruebas integrado con métodos de aserción
- Python Logging -- Logging estructurado como alternativa al debugging con print