Skip to content
Tópicos
Python
Python Assert: depure de forma mais inteligente

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 AssertionError

A 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 -1

O 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 == 80

Assertions 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 += amount

Pó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 result

Invariantes 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 -1

Invariantes 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.

Featureassertraise
PurposeCatch programmer errors (bugs)Handle runtime conditions (expected failures)
Can be disabledYes, with -O flagNo, always active
Use for input validationNeverYes
Use for external dataNeverYes
Typical exceptionAssertionErrorValueError, TypeError, RuntimeError, etc.
When it firesSomething is wrong with the codeSomething is wrong with the input/environment
AudienceThe developerThe user or calling code
Presence in productionShould not be relied uponRequired

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] == 4

Reescrita 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 == expected

Saí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" + 5

Ambas 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 -= amount

Nunca 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_value

A 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 removed

Quando 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=1 ou python -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 / b

As 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 -1

Context 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 once

Lidando 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 failed

Para 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 report

Quando 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 df

Ao 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 user

Observe 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 model

Transiçõ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 @ b

3. 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 results

Resumo 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
📚