Type checking em runtime com isinstance e typing.cast
1. Introdução ao type checking em runtime no Python
Python é uma linguagem de tipagem dinâmica, onde os tipos das variáveis são determinados em tempo de execução. Embora isso ofereça flexibilidade, também pode levar a erros difíceis de depurar quando objetos inesperados são passados para funções. O type checking em runtime — verificar tipos enquanto o programa executa — é essencial para validar dados, construir APIs robustas e implementar duck typing controlado.
As principais ferramentas para isso são:
type(): retorna o tipo exato de um objetoisinstance(): verifica se um objeto é de um tipo específico, considerando herançatyping.cast(): não faz verificação real, apenas informa o type checker sobre o tipo esperado
Neste artigo, vamos explorar cada uma dessas ferramentas, seus casos de uso e como combiná-las para criar código Python mais seguro e previsível.
2. isinstance: o pilar da verificação de tipos
isinstance() é a função mais recomendada para verificação de tipos em runtime. Diferente de type(), ela respeita hierarquias de classes e funciona com classes abstratas.
# Sintaxe básica
x = 42
print(isinstance(x, int)) # True
print(isinstance(x, float)) # False
# Verificação com tupla de tipos
print(isinstance(x, (int, float))) # True (aceita int ou float)
# Hierarquia de classes
class Animal:
pass
class Cachorro(Animal):
pass
rex = Cachorro()
print(isinstance(rex, Cachorro)) # True
print(isinstance(rex, Animal)) # True (herança)
print(type(rex) == Animal) # False (type() não considera herança)
3. isinstance com tipos genéricos e containers
Um desafio comum é verificar tipos genéricos como List[int] ou Dict[str, int]. isinstance não funciona diretamente com tipos parametrizados:
from typing import List, Dict, get_origin, get_args
# Isso NÃO funciona
# isinstance([1, 2, 3], List[int]) # TypeError
# Solução com get_origin e get_args (Python 3.8+)
def validate_list_of_ints(obj):
if not isinstance(obj, list):
return False
return all(isinstance(item, int) for item in obj)
# Validando estruturas aninhadas
def validate_user_data(data):
expected_type = Dict[str, List[int]]
origin = get_origin(expected_type) # dict
args = get_args(expected_type) # (str, List[int])
if not isinstance(data, origin):
return False
for key, value in data.items():
if not isinstance(key, args[0]):
return False
if not validate_list_of_ints(value):
return False
return True
# Exemplo prático
user_scores = {"Alice": [95, 87, 92], "Bob": [78, 85]}
print(validate_user_data(user_scores)) # True
4. typing.cast: afirmação de tipos sem verificação real
typing.cast é uma ferramenta enganosa: ela não faz absolutamente nenhuma verificação em runtime. Seu único propósito é informar ferramentas de análise estática (como mypy) sobre o tipo esperado.
from typing import cast
def process_data(data):
# cast não valida nada em runtime
numbers = cast(list[int], data)
# Se data não for uma lista, isso quebrará em runtime
return sum(numbers)
# Exemplo onde cast é útil
def get_first_element(items):
# O type checker não sabe que items[0] é int
first = items[0]
return cast(int, first) # Apenas para o type checker
# Diferença crucial entre cast e isinstance
def safe_process(data):
if isinstance(data, list):
# isinstance verificou, agora podemos usar cast com segurança
numbers = cast(list[int], data)
return sum(numbers)
return 0
5. Combinando isinstance e cast para código robusto
O padrão mais eficaz é usar isinstance para verificar em runtime e cast para ajudar o type checker:
class Usuario:
def __init__(self, nome: str, idade: int):
self.nome = nome
self.idade = idade
def get_info(self) -> str:
return f"{self.nome}, {self.idade} anos"
def processar_usuario(obj):
# Guard clause com isinstance
if not isinstance(obj, Usuario):
raise TypeError("Esperado um objeto Usuario")
# cast ajuda o type checker a entender o tipo
usuario = cast(Usuario, obj)
return usuario.get_info()
# Exemplo mais complexo com Union
from typing import Union
def processar_valor(valor: Union[int, str, float]):
if isinstance(valor, int):
# cast para int permite acesso a métodos específicos
v = cast(int, valor)
return v.to_bytes(4, 'big')
elif isinstance(valor, str):
return valor.upper()
elif isinstance(valor, float):
return round(valor, 2)
raise TypeError(f"Tipo não suportado: {type(valor)}")
6. Tratamento de tipos complexos: Union, Optional e Any
from typing import Union, Optional, Any
# Union com isinstance
def process_union(value: Union[int, str, None]):
if isinstance(value, (int, str)):
# isinstance com tupla funciona para Union
return f"Processado: {value}"
elif value is None:
return "Valor nulo"
raise TypeError("Tipo não esperado")
# Optional é Union[T, None]
def process_optional(value: Optional[int]):
# isinstance(value, int) já trata o None implicitamente
if isinstance(value, int):
return value * 2
return 0
# Armadilhas com Any
def problem_with_any(value: Any):
# Isso NÃO funciona
# isinstance(value, Any) # TypeError: Cannot parameterize Any
# Alternativa: verificar se NÃO é None
if value is not None:
return str(value)
return "None"
7. Performance e boas práticas no type checking runtime
import time
from functools import singledispatch
# Comparação de performance
data = list(range(1000000))
# type() vs isinstance()
start = time.time()
for x in data:
type(x) is int
print(f"type(): {time.time() - start:.3f}s")
start = time.time()
for x in data:
isinstance(x, int)
print(f"isinstance(): {time.time() - start:.3f}s")
# Alternativa elegante: singledispatch
@singledispatch
def process(obj):
raise TypeError(f"Tipo não suportado: {type(obj)}")
@process.register(int)
def _(obj):
return obj * 2
@process.register(str)
def _(obj):
return obj.upper()
@process.register(list)
def _(obj):
return sum(obj)
# Uso
print(process(10)) # 20
print(process("hello")) # HELLO
print(process([1,2,3])) # 6
Boas práticas:
- Prefira isinstance a type() quando herança estiver envolvida
- Use hasattr() apenas para duck typing, não para verificação de tipo
- Evite verificação excessiva em loops internos críticos de performance
- singledispatch é elegante para despacho baseado em tipo sem if-else aninhados
8. Conclusão e integração com ferramentas de type checking
O type checking em runtime em Python segue uma divisão clara:
isinstance: verificação real em runtime, ideal para validação de dados e controle de fluxocast: apenas para análise estática, sem efeito em runtime
Essas técnicas se complementam perfeitamente com ferramentas modernas:
- mypy e Pyright: usam anotações de tipo e
castpara análise estática - Pydantic: usa
isinstanceinternamente para validação automática de dados - dataclasses com validadores: combinam
isinstancecom decoradores
Para aprofundamento, explore metaclasses e descritores — técnicas avançadas que permitem controle de tipos em nível de classe, criando sistemas de validação ainda mais sofisticados.
Referências
- Documentação oficial do isinstance — Referência completa da função isinstance na documentação oficial do Python
- Documentação do typing.cast — Especificação oficial do cast e outras utilidades do módulo typing
- Guia de type checking no Python Real Python — Tutorial abrangente sobre verificação de tipos em Python, incluindo runtime e estática
- Documentação do mypy sobre casts — Guia oficial do mypy sobre como usar cast e isinstance com o type checker
- PEP 484 – Type Hints — Proposta original que introduziu type hints e o módulo typing no Python
- Python Type Checking with Pydantic — Documentação do Pydantic, biblioteca que usa isinstance para validação automática de tipos em runtime