W3docs

Type Hints em Python

Aprenda type hints em Python: como anotar variáveis, funções e classes, usar o módulo typing e verificar tipos com mypy.

Type hints permitem que você associe informações de tipo esperadas a variáveis, parâmetros de funções e valores de retorno. Python não os aplica em tempo de execução — eles são metadados consumidos por editores, linters e verificadores de tipo como o mypy para capturar bugs antes de você executar uma única linha.

Este capítulo aborda:

  • Por que type hints são importantes e quando usá-los
  • Anotando variáveis e funções
  • Tipos embutidos e o módulo typing (List, Dict, Optional, Union, Tuple, Any, Callable)
  • Sintaxe moderna (Python 3.10+)
  • Anotando classes e self
  • Genéricos e aliases de tipo
  • Análise estática com mypy
  • Armadilhas comuns

Por que usar Type Hints?

Python é tipado dinamicamente: uma variável pode conter qualquer valor de qualquer tipo. Essa flexibilidade é poderosa, mas torna bases de código grandes mais difíceis de navegar — você não consegue saber o tipo do argumento de uma função apenas lendo o local de chamada.

Type hints resolvem isso sem abrir mão do dinamismo do Python:

  • Editores exibem erros imediatamente. VS Code, PyCharm e outros destacam incompatibilidades de tipo enquanto você digita.
  • Refatoração fica mais segura. Mude a assinatura de uma função e o verificador de tipo indica cada local de chamada que quebra.
  • O código se autodocumenta. def greet(name: str) -> str comunica o contrato sem precisar de uma docstring.
  • Bibliotecas ficam mais fáceis de usar. Bibliotecas tipadas expõem autocomplete para cada atributo e método.

Type hints foram introduzidos no Python 3.5 por meio da PEP 484. A sintaxe foi refinada em cada versão principal desde então. Os exemplos abaixo indicam a versão mínima do Python onde a sintaxe ficou disponível pela primeira vez.

Anotando Variáveis

Adicione dois-pontos após o nome da variável seguido do tipo:

name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = True

Você também pode declarar o tipo de uma variável sem atribuir um valor ainda. Isso é chamado de declaração antecipada e é útil dentro de classes ou no nível do módulo:

user_id: int   # declared but not yet assigned
user_id = 42

Anotações de tipo em variáveis no nível do módulo não afetam o comportamento em tempo de execução — elas são armazenadas no dicionário __annotations__ do módulo, mas caso contrário são ignoradas pelo interpretador.

Anotando Funções

Coloque anotações nos parâmetros (após os dois-pontos) e no valor de retorno (após -> antes dos dois-pontos que encerram a assinatura):

def add(a: int, b: int) -> int:
    return a + b

def greet(name: str) -> str:
    return f"Hello, {name}!"

def send_email(to: str, subject: str, body: str) -> None:
    print(f"Sending '{subject}' to {to}")

result: int = add(3, 5)
message: str = greet("Alice")

-> None significa que a função não possui valor de retorno significativo (ela retorna None implicitamente). Omitir a anotação de retorno também é válido, mas o -> None explícito deixa a intenção clara.

Parâmetros com Valor Padrão

Os valores padrão vão após a anotação:

def connect(host: str, port: int = 8080, secure: bool = False) -> None:
    print(f"Connecting to {host}:{port} (secure={secure})")

connect("example.com")          # uses defaults
connect("example.com", 443, True)

*args e **kwargs

Anote o tipo do elemento, não o tipo da coleção:

def total(*prices: float) -> float:
    return sum(prices)

def create_user(**fields: str) -> dict:
    return fields

print(round(total(9.99, 4.50, 12.00), 2))   # 26.49
print(create_user(name="Bob", role="admin"))

*prices: float significa que cada argumento posicional é um float; em tempo de execução, prices ainda é uma tuple regular de floats. Da mesma forma, **fields: str significa que o valor de cada argumento nomeado é uma str.

O Módulo typing

Para qualquer coisa além dos tipos embutidos básicos, importe do módulo typing (Python 3.5+). A partir do Python 3.9, muitos tipos do typing foram incorporados diretamente nos equivalentes embutidos (veja Sintaxe Moderna abaixo).

List, Tuple, Set, Dict

from typing import List, Tuple, Set, Dict

def first_names(users: List[str]) -> str:
    return users[0] if users else ""

def dimensions() -> Tuple[int, int, int]:
    return (1920, 1080, 32)

def unique_tags(items: List[str]) -> Set[str]:
    return set(items)

def word_count(text: str) -> Dict[str, int]:
    counts: Dict[str, int] = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

print(first_names(["Alice", "Bob"]))        # Alice
print(dimensions())                         # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"]))     # {'py', 'web'}
print(word_count("one two one"))            # {'one': 2, 'two': 1}

Optional

Optional[X] é uma abreviação para Union[X, None]. Use-o sempre que um valor pode estar ausente:

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    db = {1: "Alice", 2: "Bob"}
    return db.get(user_id)   # returns None if not found

name = find_user(1)
if name is not None:
    print(name.upper())      # ALICE

missing = find_user(99)
print(missing)               # None

Um verificador de tipo vê Optional[str] e sabe que você deve verificar None antes de chamar métodos de string no resultado. Sem a verificação, ele relata um erro.

Union

Union[X, Y] significa que o valor pode ser do tipo X ou do tipo Y:

from typing import Union

def stringify(value: Union[int, float, str]) -> str:
    return str(value)

print(stringify(42))      # 42
print(stringify(3.14))    # 3.14
print(stringify("hi"))    # hi

Union é mais útil quando uma função genuinamente aceita múltiplos tipos não relacionados. Se você se pegar escrevendo Union[str, None], use Optional[str] — é mais idiomático.

Callable

Callable[[ArgTypes...], ReturnType] anota uma função passada como argumento:

from typing import Callable

def apply_twice(func: Callable[[int], int], value: int) -> int:
    return func(func(value))

def double(n: int) -> int:
    return n * 2

print(apply_twice(double, 3))   # 12

Callable[[int], int] significa: um callable que recebe um argumento int e retorna um int. Se a lista de argumentos for complexa ou desconhecida, use Callable[..., ReturnType].

Any

Any é um tipo especial que desativa a verificação de tipo para aquele valor. Todo tipo pode ser atribuído a Any e de Any:

from typing import Any

def log(value: Any) -> None:
    print(value)

log(42)
log("hello")
log([1, 2, 3])

Use Any com moderação — é uma válvula de escape que remove a própria proteção que os type hints fornecem. É apropriado ao interagir com código de terceiros sem tipagem, ou durante a migração gradual de uma base de código grande.

Sintaxe Moderna (Python 3.9+, 3.10+)

Genéricos embutidos (Python 3.9+)

A partir do Python 3.9, você pode usar os tipos embutidos diretamente como genéricos, sem precisar importar de typing:

# Python 3.9+
def word_count(text: str) -> dict[str, int]:
    counts: dict[str, int] = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

def first(items: list[int]) -> int | None:
    return items[0] if items else None

print(word_count("cat dog cat"))    # {'cat': 2, 'dog': 1}
print(first([10, 20, 30]))         # 10
print(first([]))                   # None

Use list[str] em vez de List[str], dict[str, int] em vez de Dict[str, int], e assim por diante.

Sintaxe de união X | Y (Python 3.10+)

O Python 3.10 introduziu o operador | para uniões, substituindo Union[X, Y] e Optional[X]:

# Python 3.10+
def parse(value: str | int | None) -> str:
    if value is None:
        return "nothing"
    return str(value)

print(parse("hello"))   # hello
print(parse(42))        # 42
print(parse(None))      # nothing

str | None é equivalente a Optional[str]. Essa sintaxe é mais limpa e fácil de ler.

Anotando Classes

Anote atributos de instância dentro de __init__ e adicione anotações de retorno aos métodos:

class BankAccount:
    owner: str        # class-level annotation (no default value)
    balance: float

    def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
        self.owner = owner
        self.balance = initial_balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount

    def withdraw(self, amount: float) -> bool:
        if amount > self.balance:
            return False
        self.balance -= amount
        return True

    def __repr__(self) -> str:
        return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"

account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0))   # True
print(account)                  # BankAccount(owner='Alice', balance=120.00)

A anotação em self é sempre inferida — você nunca escreve self: BankAccount. O tipo de retorno de __init__ é sempre None.

ClassVar

Use ClassVar[T] (do módulo typing) para marcar um atributo que pertence à classe, não a cada instância:

from typing import ClassVar

class Config:
    MAX_RETRIES: ClassVar[int] = 3
    timeout: int

    def __init__(self, timeout: int) -> None:
        self.timeout = timeout

print(Config.MAX_RETRIES)   # 3

Um verificador de tipo avisa se você tentar definir um ClassVar em uma instância — ele é destinado a ser compartilhado no nível da classe.

Aliases de Tipo

Um alias de tipo dá a um tipo longo ou complexo um nome mais curto e significativo:

from typing import List, Tuple

# Simple alias
UserID = int
Filename = str

# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]

def distance(p1: Coordinates, p2: Coordinates) -> float:
    return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5

print(distance((0.0, 0.0), (3.0, 4.0)))   # 5.0

A partir do Python 3.12, use a instrução type para aliases explícitos e inspecionáveis:

# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]

Genéricos com TypeVar

TypeVar permite escrever uma única função que funciona com qualquer tipo, preservando as relações de tipo:

from typing import TypeVar, List

T = TypeVar("T")

def first_item(items: List[T]) -> T:
    return items[0]

x: int = first_item([1, 2, 3])       # x is int
s: str = first_item(["a", "b"])      # s is str

O verificador de tipo infere a partir do argumento o que é T e carrega essa informação até o tipo de retorno. Sem TypeVar, você teria que retornar Any e perder a segurança de tipos.

Você pode restringir TypeVar a um conjunto de tipos permitidos:

from typing import TypeVar

Numeric = TypeVar("Numeric", int, float)

def double(n: Numeric) -> Numeric:
    return n * 2

print(double(4))      # 8   (int)
print(double(2.5))    # 5.0 (float)

Verificação Estática de Tipos com mypy

mypy é o verificador de tipos estático mais amplamente usado para Python. Instale-o com pip:

pip install mypy

Em seguida, execute-o em um arquivo:

mypy my_script.py

Exemplo: capturando um bug com mypy

Salve o seguinte como demo.py:

def greet(name: str) -> str:
    return f"Hello, {name}!"

result = greet(42)   # passing int instead of str
print(result.upper())

Executar mypy demo.py reporta:

demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

O próprio Python executa o código normalmente (f-strings coagem qualquer tipo), mas o mypy detectou a incompatibilidade antes de você descobri-la em produção.

Opções úteis do mypy

FlagEfeito
--strictAtiva todas as verificações opcionais (recomendado para novos projetos)
--ignore-missing-importsSuprime erros sobre stubs de terceiros ausentes
--check-untyped-defsTambém verifica o tipo de funções sem anotações
--disallow-untyped-defsExige anotações em todas as definições de função

Um mypy.ini (ou [tool.mypy] em pyproject.toml) mantém a configuração fora da linha de comando:

[mypy]
strict = true
ignore_missing_imports = true

Tipagem Gradual

Você não precisa anotar cada função de uma vez. Python suporta tipagem gradual: código anotado e não anotado coexistem pacificamente. mypy pula funções não anotadas por padrão (a menos que --check-untyped-defs esteja ativado).

Uma abordagem prática para uma base de código existente:

  1. Adicione anotações ao novo código desde o início.
  2. Anote primeiro as funções mais chamadas ou mais propensas a erros.
  3. Habilite --strict módulo por módulo conforme a cobertura melhorar.
  4. Use Any apenas onde uma biblioteca de terceiros não for tipada, e adicione um comentário explicando o motivo.

Armadilhas Comuns

Referências antecipadas

Se um tipo se refere a uma classe definida mais adiante no mesmo arquivo, coloque o nome entre aspas para torná-lo uma string (uma referência antecipada):

class Node:
    def __init__(self, value: int, next: "Node | None" = None) -> None:
        self.value = value
        self.next = next

head = Node(1, Node(2))
print(head.value, head.next.value)   # 1 2

A partir do Python 3.10+, adicione from __future__ import annotations no topo do arquivo. Isso torna todas as anotações strings preguiçosas e elimina a necessidade de aspas manuais.

Anotações em tempo de execução

Por padrão, as anotações no Python 3.9 e versões anteriores são avaliadas imediatamente. Isso significa que uma referência antecipada sem aspas gera um NameError:

# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...

Com from __future__ import annotations (Python 3.7+), todas as anotações são armazenadas como strings e avaliadas apenas quando inspecionadas — resolvendo o problema de referências antecipadas automaticamente.

None vs Optional

Um erro comum é anotar um tipo de retorno como str quando a função pode realmente retornar None. Sempre use Optional[str] (ou str | None) quando None é um retorno possível:

from typing import Optional

# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
    if user_id == 0:
        return None   # type: ignore  — this is the bug

# Correct
def get_name_safe(user_id: int) -> Optional[str]:
    if user_id == 0:
        return None
    return "Alice"

list vs List (compatibilidade de versão)

Se seu código roda no Python 3.8 ou anterior, você deve usar from typing import List e escrever List[str]. No Python 3.9+, list[str] funciona diretamente. Se precisar suportar ambos, use as importações do typing ou adicione from __future__ import annotations.

Referência Rápida

AnotaçãoSignificado
x: intVariável x é um inteiro
def f(a: str) -> boolParâmetro a é str; valor de retorno é bool
-> NoneFunção não retorna nada significativo
Optional[str]str ou None
Union[int, str]int ou str
list[int] / List[int]Lista de inteiros
dict[str, int] / Dict[str, int]Dict mapeando str para int
tuple[int, str] / Tuple[int, str]Tuple de (int, str)
Callable[[int], str]Função que recebe int e retorna str
AnyQualquer tipo (desativa a verificação)
ClassVar[T]Atributo no nível da classe
TypeVar("T")Variável de tipo genérico

Tópicos Relacionados

Prática

Prática
What does Optional[str] mean in a Python type hint?
What does Optional[str] mean in a Python type hint?
Prática
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Prática
What is the purpose of TypeVar in the typing module?
What is the purpose of TypeVar in the typing module?
Was this page helpful?