Python Assert: depure de forma mais inteligente
Atualizado em
Você está depurando uma função de processamento de dados. Em algum ponto entre ler o CSV e gravar a saída, uma coluna de inteiros virou floats, uma lista que nunca deveria estar vazia ficou vazia e um ID de usuário que deveria ser positivo agora é -1. Você espalha print() por toda parte, executa o script de novo, tenta entender a saída e repete. Uma hora depois, encontra o bug -- uma única função estava aceitando entrada inválida silenciosamente e repassando dados corrompidos adiante. A falha real aconteceu cinquenta linhas antes do crash, e nada avisou você.
Esse é um problema universal no desenvolvimento em Python. Bugs se propagam silenciosamente. Um valor None passa por três chamadas de função antes de causar um AttributeError. Um índice negativo de array faz wrap e aponta para o elemento errado. Um dicionário que deveria ter cinco chaves só tem quatro, e a chave ausente causa um erro lógico sutil que só aparece em produção. Quando o erro se torna visível, você já perdeu todo o contexto sobre onde as coisas realmente deram errado.
A instrução assert do Python resolve isso ao capturar bugs exatamente no ponto de falha, com uma mensagem clara sobre o que aconteceu. Em vez de torcer para que dados ruins eventualmente causem uma falha óbvia, você declara suas suposições explicitamente -- e o Python as valida imediatamente.
O que é a instrução Assert?
A instrução assert testa uma condição. Se a condição for True, nada acontece. Se for False, o Python levanta um AssertionError imediatamente.
assert 2 + 2 == 4 # Passa silenciosamente
assert 2 + 2 == 5 # Levanta AssertionErrorA sintaxe básica é:
assert condição
assert condição, "Mensagem de erro explicando o que deu errado"Internamente, o Python traduz assert para uma instrução if. Este é o equivalente exato:
# assert condição, mensagem
# é equivalente a:
if __debug__:
if not condition:
raise AssertionError(message)A variável __debug__ é True por padrão. Ela só se torna False quando o Python é executado com a flag -O (otimização). Isso significa que as assertions podem ser completamente desativadas em produção -- um recurso com implicações importantes que veremos mais adiante.
Veja o que acontece quando uma assertion falha:
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 -1O traceback aponta para a linha exata em que a suposição foi violada, e a mensagem diz exatamente o que deu errado. Compare isso com um misterioso IndexError vinte linhas depois, porque valores negativos se propagaram sem verificação.
Uso Básico do Assert
Assertions Simples
As assertions mais simples verificam uma única condição:
# Verifica se uma variável não é None
config = load_config()
assert config is not None
# Verifica se uma lista não está vazia
items = get_items()
assert len(items) > 0
# Verifica uma propriedade matemática
result = calculate_discount(price=100, percent=20)
assert result == 80Assertions com Mensagens Personalizadas
Sempre inclua uma mensagem. Sem ela, uma assertion que falha quase não fornece contexto:
# Ruim: sem mensagem
assert len(users) > 0
# Bom: mensagem descritiva
assert len(users) > 0, "A lista de usuários está vazia -- a consulta ao banco pode ter falhado"
# Bom: inclui o valor real
assert temperature >= -273.15, f"Temperatura {temperature}C está abaixo do zero absoluto"A mensagem é o segundo argumento de assert, separado por vírgula. Ela pode ser qualquer expressão que produza uma string, incluindo f-strings com valores em tempo de execução:
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 com Parênteses -- Uma Armadilha Comum
Existe um bug sutil que pega muitos desenvolvedores Python:
# WARNING: This assertion NEVER fails!
assert(condition, "error message")Isso cria uma tupla (condition, "error message"). Uma tupla não vazia é sempre truthy, então a assertion sempre passa. O Python ainda pode avisar:
SyntaxWarning: assertion is always true, perhaps remove parentheses?A forma correta é:
# 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 com Condições Complexas
Múltiplas Condições
Você pode combinar condições com and, or e 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...Verificações de Tipo com isinstance
Use assertions com isinstance para verificar tipos de dados durante o desenvolvimento:
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 verificação de tipos em produção, considere usar Python type hints com um verificador estático como o mypy. As assertions servem para capturar bugs durante o desenvolvimento, não para impor tipos em tempo de execução.
Verificações de Contêineres e Coleções
# 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]}"
)Padrões Comuns de Depuração com Assert
Pré-condições de Função
Pré-condições verificam se uma função recebe entrada válida antes de fazer qualquer trabalho. Coloque-as no início da função:
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 += amountPós-condições de Função
Pós-condições verificam se uma função produziu a saída correta antes de retornar. Coloque-as imediatamente antes da instrução 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 Loop
Invariantes de loop verificam se uma condição permanece verdadeira em cada iteração de um laço. Elas detectam erros de off-by-one, loops infinitos e 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 Classe
Invariantes de classe verificam se o estado interno de um objeto continua consistente após cada operação:
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: Quando Usar Cada Um
Esta é uma das distinções mais importantes no tratamento de erros em Python. assert e raise servem a propósitos fundamentalmente diferentes.
| 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 |
Use assert for: Internal invariants and developer assumptions
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
...Use raise for: Input validation and expected error conditions
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.
...A diferença crítica: se alguém executar python -O your_script.py, toda instrução assert é completamente removida. Se você usar assert para validação de entrada, sua validação desaparece no modo otimizado. Isso não é um risco teórico -- muitas ferramentas de deployment e ambientes de produção usam a flag -O. Para um aprofundamento em padrões de tratamento de exceções, veja o Python try/except guide.
Regra Prática
Pergunte a si mesmo: "Se essa verificação fosse removida completamente, um usuário poderia causar um problema de segurança ou corrupção de dados?" Se sim, use raise. Se a verificação apenas captura erros do desenvolvedor (bugs no próprio código), use assert.
Assert em Testes
Assertions são a base dos testes em Python. Tanto unittest quanto pytest dependem delas para verificar o comportamento esperado.
Assertions no pytest
O pytest usa instruções assert normais em vez de métodos especiais de assertion. Essa é uma das suas maiores vantagens -- você escreve Python natural em vez de decorar nomes 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] == 4Reescrita de Assert no pytest
O que torna o pytest especial é a reescrita de assert. Quando um assert simples falha, o Python só informa AssertionError. O pytest reescreve suas instruções assert no momento do import para fornecer mensagens ricas de falha:
def test_comparison():
result = {"name": "Alice", "age": 30}
expected = {"name": "Alice", "age": 31}
assert result == expectedSaída do pytest:
FAILED test_example.py::test_comparison - AssertionError: assert {'age': 30, 'name': 'Alice'} == {'age': 31, 'name': 'Alice'}
Differing items:
{'age': 30} != {'age': 31}Sem a reescrita do pytest, você veria apenas AssertionError, sem detalhes. Essa mágica funciona porque o pytest usa um import hook para transformar instruções assert em verificações mais verbosas que capturam valores intermediários.
Padrões Comuns de Assertion no 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"Assertions em unittest
O módulo unittest fornece assertions baseadas em métodos em vez de assert puro. Elas oferecem mensagens de erro melhores sem depender da reescrita do 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" + 5Ambas as abordagens são válidas. O assert puro do pytest é mais legível e Pythonic. As assertions baseadas em métodos do unittest fornecem mensagens detalhadas sem reescrita em tempo de importação.
Testando Assertions Interativamente
Ao desenvolver e depurar assertions de teste interativamente, ferramentas como RunCell (opens in a new tab) permitem executar células de teste individuais em notebooks Jupyter com feedback instantâneo. Isso é especialmente útil quando você está construindo condições complexas de assertion passo a passo -- você pode testar cada assertion isoladamente antes de combiná-la em uma suíte de testes completa.
Quando NÃO Usar Assert
Esta seção é crítica. Usar assert de forma errada cria bugs sutis e perigosos que só aparecem em produção.
Nunca Use Assert para Validação 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 Use Assert para Dados de Fontes Externas
Qualquer dado vindo de usuários, arquivos, redes, bancos de dados ou APIs pode estar malformado. Essas verificações devem sempre ser executadas:
# 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 Use Assert para Verificações de Segurança
# 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)Com python -O, a versão com assert permite que qualquer usuário exclua qualquer outro usuário. Isso é uma vulnerabilidade de segurança real.
Nunca Use Assert com Efeitos Colaterais
Como as assertions podem ser desativadas, a expressão dentro delas nunca deve ter efeitos colaterais:
# 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_valueA Flag -O: Como as Assertions Desaparecem
O Python tem dois níveis de otimização que afetam assertions:
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 removedQuando o Python roda com -O, o interpretador define __debug__ como False e remove completamente todas as instruções assert do bytecode. Elas não são apenas ignoradas -- deixam de existir. A condição nunca é avaliada, e a mensagem de erro nunca é construída.
Você pode verificar isso:
# 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.Onde o -O é Usado na Prática
- Docker images: Muitos Dockerfiles de produção usam
PYTHONOPTIMIZE=1oupython -O - Deployment tools: Alguns servidores WSGI executam Python em modo otimizado
- Performance-sensitive applications: Remover assertions pode acelerar loops apertados
- Library code: Bibliotecas nunca devem assumir que assertions estão ativas, porque os consumidores controlam o nível de otimização
Implicações para o Seu Código
Pense nas assertions como andaimes durante a construção. Elas sustentam a estrutura enquanto você constrói, mas são removidas quando o prédio fica pronto. Seu código deve estar correto com ou sem assertions.
# 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 / bAs assertions ajudam a capturar bugs durante o desenvolvimento. O raise trata a condição de erro esperada em produção. Ambas as camadas servem a propósitos diferentes.
Helpers Personalizados de Assertion
Quando você perceber que está escrevendo o mesmo padrão de assertion repetidamente, extraia isso para uma função auxiliar reutilizável.
Funções Simples de Assertion
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)Assertions via Decorator
Você pode usar decorators para adicionar verificações de pré/pós-condição às funções sem poluir o corpo da função:
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 -1Context Manager para Grupos de Assertion
Quando você precisa verificar múltiplas condições relacionadas e reportar todas as falhas de uma 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 onceLidando com AssertionError com Try/Except
Você pode capturar AssertionError como qualquer outra exceção, embora isso raramente seja uma boa ideia em código de aplicação:
try:
assert len(data) > 0, "Data is empty"
process(data)
except AssertionError as e:
print(f"Assertion failed: {e}")
# Handle the failure...Quando Capturar AssertionError Faz Sentido
Há alguns casos legítimos de uso:
1. Frameworks de teste: pytest e unittest capturam AssertionError para relatar falhas de teste em vez de encerrar com crash.
2. Logging de falhas de assertion em processos de longa duração:
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 padrões de logging em produção, capturar AssertionError deve ser combinado com tratamento adequado de exceções para garantir que as falhas fiquem visíveis sem derrubar o processo inteiro.
3. Degradação graciosa em caminhos não críticos:
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 reportQuando NÃO Capturar AssertionError
Não capture AssertionError para suprimir bugs silenciosamente. O objetivo das assertions é tornar os bugs barulhentos e visíveis:
# WRONG: Silencing assertions defeats their purpose
try:
assert user.is_valid()
except AssertionError:
pass # Who cares?Exemplos do Mundo Real
Validação de Pipeline de Dados
Assertions são valiosas em pipelines de processamento de dados onde transformações precisam preservar certas propriedades:
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 dfAo trabalhar com DataFrames em fluxos de data science, PyGWalker (opens in a new tab) permite transformar DataFrames validados em visualizações interativas para exploração adicional -- um próximo passo natural depois que suas assertions de pipeline confirmam que os dados estão limpos e prontos para análise.
Verificação de Resposta 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 userObserve a distinção: raise lida com condições de erro esperadas (problemas de rede, códigos de status ruins). assert captura coisas que indicam um bug na API ou nas suposições do seu código sobre a API.
Checks de Sanidade em 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 modelTransições 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"Considerações de Desempenho
Quanto Custam as Assertions?
As assertions têm um custo pequeno, mas mensurável. A expressão da condição é avaliada toda vez que a assertion roda. Para verificações simples como assert x > 0, isso é irrelevante. Para verificações caras, o custo pode se acumular:
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")Estratégias para Assertions Caras
Se uma assertion for lenta demais para um loop apertado, você tem várias opções:
1. Verifique uma amostra em vez do dataset inteiro:
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. Use a flag __debug__ para verificações caras condicionais:
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. Faça assert nas bordas, não dentro de loops 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 resultsResumo das Melhores Práticas
Aqui estão os princípios principais para um uso eficaz das assertions:
1. Sempre inclua uma mensagem. assert x > 0 não diz nada quando falha. assert x > 0, f"Expected positive value, got {x}" diz tudo.
2. Nunca use assert para validação de entrada. Entrada de usuário, conteúdo de arquivo, respostas de API e consultas ao banco podem estar malformados. Valide com if/raise.
3. Use assert para invariantes internas. Coisas que devem ser verdade se seu código estiver correto: pré-condições de função, pós-condições, invariantes de loop, invariantes de classe.
4. Nunca coloque efeitos colaterais dentro de assert. A expressão assert items.pop() == expected remove um item da lista -- mas só quando assertions estão habilitadas.
5. Use assert livremente durante o desenvolvimento. Elas não custam nada quando desativadas com -O e economizam horas de depuração quando ativadas.
6. Mantenha as mensagens de assertion acionáveis. Inclua o valor real, o valor esperado e contexto suficiente para entender o que deu errado.
7. Teste suas assertions. Escreva testes que verifiquem se suas assertions detectam os bugs que deveriam detectar.
import pytest
def test_transfer_rejects_negative_amount():
with pytest.raises(AssertionError, match="positive"):
transfer_money(account_a, account_b, amount=-100)FAQ
Conclusão
A instrução assert do Python é uma ferramenta leve e poderosa para programação defensiva. Ela transforma suposições implícitas em verificações explícitas e impostas, capturando bugs no ponto de falha em vez de deixar dados ruins se propagarem pelo seu código. Usadas corretamente, assertions tornam a depuração mais rápida, o código mais legível e os invariantes autoexplicativos.
As regras principais são diretas: use assert para invariantes internos e suposições do desenvolvedor, use raise para validação de entrada e condições de erro esperadas, sempre inclua mensagens descritivas e nunca coloque efeitos colaterais em expressões de assert. Em testes, tanto pytest quanto unittest dependem fortemente de assertions para verificar o comportamento esperado.
Para fluxos de trabalho de data science e análise, assertions combinam naturalmente com ferramentas como PyGWalker (opens in a new tab) para validar DataFrames antes da visualização, e ambientes interativos como RunCell (opens in a new tab) para construir e testar iterativamente pipelines de dados protegidos por assertions em notebooks Jupyter.
Domine esses padrões e suas sessões de depuração ficarão mais curtas, seu código ficará mais robusto e seus testes serão mais expressivos.
Guias Relacionados
- Python Try/Except -- Tratamento de exceções para erros em tempo de execução
- Python Type Hints -- Verificação estática de tipos como complemento às assertions em tempo de execução
- Python Decorators -- Wrappers reutilizáveis de funções, incluindo decorators de assertion
- Python unittest -- Framework de testes embutido com métodos de assertion
- Python Logging -- Logging estruturado como alternativa à depuração com print