
Passo 1 de 8
Partir das operações, não do tipo concreto
Escolha anotações a partir do que a função faz e do que sua API promete, não apenas dos valores usados em um exemplo.
Trilha de aprendizado · Nível 12 · Tutorial 6
Anotar entradas e saídas conforme as operações realmente necessárias, permitindo diferentes coleções e fontes incrementais sem exigir tipos concretos por conveniência.
Partir das operações, não do tipo concreto
Escolha anotações a partir do que a função faz e do que sua API promete, não apenas dos valores usados em um exemplo. 2 min
Receber elementos com Iterable
Generalize parâmetros que apenas são percorridos, preservando o tipo dos elementos sem exigir list. 2 min
Expor avanço e produção com Iterator
Use Iterator quando a função avança uma fonte recebida ou promete esse avanço a quem a chama; anote geradores pelo tipo de cada valor produzido. 3 min
Exigir posição e comprimento com Sequence
Escolha Sequence quando a função precisa consultar o tamanho e acessar elementos por posição, sem restringir desnecessariamente a entrada a list. 2 min
Consultar por chave com Mapping
Generalize parâmetros de consulta de dict para Mapping sem perder os tipos de chaves e valores. 2 min
Reconhecer garantias que faltam
Identifique limites de contratos de coleção e iteração que precisam ser tratados ou documentados pela API. 3 min
Separar consulta de imutabilidade
Entenda o que Sequence e Mapping restringem na assinatura — e o que eles não mudam no objeto recebido. 2 min
Refatorar e verificar os contratos de uma pequena API
Aplique Iterable, Iterator, Sequence e Mapping em uma API curta, verifique o código localmente e registre as garantias que ainda dependem da implementação ou da documentação. 4 min

Passo 1 de 8
Escolha anotações a partir do que a função faz e do que sua API promete, não apenas dos valores usados em um exemplo.
Uma chamada pode passar uma list ou um dict, mas isso não torna esses tipos requisitos da função. Antes de anotar um parâmetro, observe quais operações o corpo realmente executa.
Se uma função só lê valores, uma anotação concreta pode restringir chamadas válidas sem trazer benefício. Em vez de perguntar “qual objeto apareceu no exemplo?”, pergunte: “de que operações esta implementação precisa?”
A representação concreta usada por quem chama e o contrato exigido pela função são decisões diferentes.

A lista à esquerda é uma possibilidade de entrada; as operações no centro definem o contrato necessário.
O módulo collections.abc disponibiliza contratos prontos para anotações. Eles descrevem operações oferecidas por um objeto; você não precisa criar classes próprias para usá-los.
Nas próximas etapas, você vai escolher contratos específicos conforme a necessidade de percorrer, avançar, acessar posições ou consultar chaves. Por enquanto, retenha o critério: selecione a interface menos restritiva que ainda cubra todas as operações usadas.
A função só percorre os nomes; ela não usa nenhuma operação exclusiva de list.
def exibir_nomes(nomes: list[str]) -> None:
for nome in nomes:
print(nome)
exibir_nomes(["Ana", "Bia"])
# Uma tupla ou uma fonte produzida sob demanda também poderia servir,
# se o contrato escolhido exigisse somente as operações do laço.Dica
Para cada parâmetro, liste mentalmente as operações usadas no corpo: percorrer, medir, acessar por posição, consultar por chave ou alterar. Só então escolha a anotação.
A entrada deve aceitar tudo o que a implementação consegue usar corretamente. Já o retorno deve expressar o que sua API quer oferecer a quem chama.
Assim, uma função pode aceitar uma interface ampla e ainda devolver uma list[str] se pretende entregar um resultado materializado, reutilizável e próprio para operações de lista. Generalizar toda saída automaticamente pode enfraquecer uma promessa útil da API.
Exemplo
def nomes_em_maiusculas(nomes: ...) -> list[str]:
return [nome.upper() for nome in nomes]Mesmo que a entrada possa ser mais geral que list[str], retornar list[str] é intencional: a função constrói e entrega uma nova lista. A anotação de retorno descreve essa oferta, não apenas a forma mínima de produzir o resultado.
Associe cada situação à conclusão correta sobre a anotação.
Toque em um item e depois no par correspondente.

Passo 2 de 8
Generalize parâmetros que apenas são percorridos, preservando o tipo dos elementos sem exigir list.
Use Iterable[T] quando a função precisa apenas obter valores de tipo T em uma iteração, por exemplo, com for.
Em vez de exigir list[T], a assinatura passa a aceitar diversas fontes compatíveis: listas, tuplas homogêneas, conjuntos e geradores. A escolha é válida quando a implementação não depende de posição, tamanho ou avanço explícito.
Todas estas fontes podem alimentar uma função que faz uma única passagem com for.

Iterable[str] descreve a capacidade de fornecer strings durante a iteração, não o tipo concreto da fonte.
A implementação já faz somente uma passagem. Portanto, apenas o contrato do parâmetro precisa mudar.
from collections.abc import Iterable
def juntar_linhas(linhas: Iterable[str]) -> str:
partes: list[str] = []
for linha in linhas:
partes.append(linha.strip())
return " | ".join(partes)
print(juntar_linhas([" um\n", "dois\n"]))
print(juntar_linhas((" três\n", "quatro\n")))
print(juntar_linhas({" cinco\n", "seis\n"}))
print(juntar_linhas(linha for linha in [" sete\n", "oito\n"]))Exemplo
Com linhas: Iterable[str], o verificador aceita fontes que fornecem str por iteração.
Mas a própria anotação não disponibiliza len(linhas), linhas[0] nem next(linhas): essas operações exigem contratos diferentes. Não escolha Iterable se a função realmente precisa delas.
Se uma função genérica devolve um elemento que recebeu, mantenha o mesmo parâmetro de tipo. Troque apenas o contêiner concreto pelo contrato necessário.
T continua ligando o tipo produzido ao tipo dos elementos fornecidos.
from collections.abc import Iterable
def primeiro[T](valores: Iterable[T]) -> T:
for valor in valores:
return valor
raise ValueError("esperado ao menos um valor")
numero = primeiro((10, 20, 30)) # T é int
palavra = primeiro(nome for nome in ["Ana", "Bia"]) # T é strComplete a importação e a assinatura abaixo com o contrato adequado.
from collections.abc import ____
def totalizar[T](valores: ____[T], converter: Callable[[T], int]) -> int:
return sum(converter(valor) for valor in valores)Preencha somente o nome do contrato que substitui os dois espaços.
Qual chamada é compatível com juntar_linhas(linhas: Iterable[str])?

Passo 3 de 8
Use Iterator quando a função avança uma fonte recebida ou promete esse avanço a quem a chama; anote geradores pelo tipo de cada valor produzido.
Iterator[T] é um contrato para uma fonte que pode ser percorrida e avançada com next(). Use-o no parâmetro quando a função precisa consumir o estado do objeto recebido.
Se a função só precisa percorrer valores, Iterable[T] continua sendo o contrato menos restritivo. Ela pode obter um iterador local com iter(fonte) sem alterar o contrato de entrada.
Compare o que cada contrato disponibiliza diretamente para a implementação.

Iterator[T] acrescenta o avanço com next() ao contrato de iteração.
O retorno preserva o tipo do próximo valor.
from collections.abc import Iterator
def retirar_proximo[T](fonte: Iterator[T]) -> T:
return next(fonte)
numeros = iter([10, 20, 30])
print(retirar_proximo(numeros)) # 10
print(retirar_proximo(numeros)) # 20Exemplo
retirar_proximo chama next(fonte) diretamente. Portanto, fonte precisa ser um Iterator[T].
Já uma função que só deseja iniciar uma passagem poderia receber Iterable[T] e fazer cursor = iter(fonte) internamente. A diferença é quem deve fornecer o objeto avançável: a função chamadora, no primeiro caso; a própria implementação, no segundo.
Dica
Anote um retorno como Iterator[T] quando a API pretende permitir que quem chamou use next(resultado). Se a API promete apenas que o resultado pode ser percorrido, Iterable[T] expressa uma interface de retorno mais limitada.
Uma função que contém yield não devolve imediatamente cada valor produzido. Ao ser chamada, ela cria e retorna um objeto iterador.
Na anotação Iterator[str], str é o tipo de cada valor produzido por yield — não o tipo do objeto devolvido pela chamada.
Cada execução de yield produz uma str; por isso, o retorno é Iterator[str].
from collections.abc import Iterator
def codigos_ativos() -> Iterator[str]:
yield "A-10"
yield "B-20"
cursor = codigos_ativos()
print(next(cursor)) # A-10
print(next(cursor)) # B-20Exemplo
Uma função geradora pode ser anotada como Iterator[str] quando o avanço explícito faz parte da interface oferecida. Se você quer expor somente a possibilidade de percorrer os valores, pode declarar o retorno como Iterable[str]; o gerador ainda é compatível, mas next() não estará disponível pelo contrato declarado.
Qual anotação deve substituir ????
def ler_cabecalho(fonte: ???) -> str:
return next(fonte)Complete a anotação:
def pares(limite: int) -> Iterator[_____]:
for numero in range(limite):
if numero % 2 == 0:
yield numero
Passo 4 de 8
Escolha Sequence quando a função precisa consultar o tamanho e acessar elementos por posição, sem restringir desnecessariamente a entrada a list.
Use Sequence[T], de collections.abc, quando a implementação precisa de uma coleção ordenada por posições. Esse contrato disponibiliza iteração, len(...) e acesso por índice, como itens[0].
Assim, uma função que consulta o primeiro e o último elemento não precisa exigir list[T]: ela precisa de uma sequência.
Compare os contratos pelo que a função realmente faz.

Sequence[T] atende a iteração, comprimento e posições; uma fonte apenas iterável não promete essas operações.
A função usa len e índices; por isso, Sequence[T] é o contrato mínimo adequado.
from collections.abc import Sequence
def extremos[T](itens: Sequence[T]) -> tuple[T, T]:
if len(itens) == 0:
raise ValueError("a sequência não pode estar vazia")
return itens[0], itens[-1]
print(extremos([10, 20, 30])) # (10, 30)
print(extremos(("a", "b", "c"))) # ('a', 'c')O mesmo parâmetro T aparece na entrada e no retorno: para Sequence[int], o resultado é tuple[int, int]; para Sequence[str], é tuple[str, str].
Listas e tuplas homogêneas compatíveis podem ser passadas. Já um gerador fornece valores por iteração, mas não oferece len nem itens[0] pelo contrato Iterable[T].
Dica
Se a função só faz for item in itens, prefira Iterable[T]. Se ela usa len(itens) ou itens[indice], Sequence[T] expressa exatamente a necessidade.
Relacione cada comportamento ao contrato de entrada menos restritivo apropriado.
Toque em um item e depois no par correspondente.

Passo 5 de 8
Generalize parâmetros de consulta de dict para Mapping sem perder os tipos de chaves e valores.
Use Mapping[K, V] quando a função precisa consultar pares de chave e valor, mas não precisa alterar o mapeamento. Esse contrato preserva os tipos: K representa as chaves e V, os valores.
Ele permite acesso por chave (dados[chave]), teste de pertencimento (chave in dados) e iteração por pares com dados.items(). Um dict pode ser passado para um parâmetro Mapping, mas a função não fica limitada a exigir um dicionário concreto.

Mapping[K, V] modela leitura de chaves e valores, não escrita.
A chave recebida tem o mesmo tipo K das chaves do mapeamento, e o resultado tem tipo V.
from collections.abc import Mapping
def obter[K, V](dados: Mapping[K, V], chave: K) -> V:
return dados[chave]
precos = {"café": 8.5, "pão": 2.0}
preco_cafe = obter(precos, "café")
print(preco_cafe) # 8.5
Exemplo
Em for chave in dados:, a variável recebe apenas as chaves. Quando o trabalho relaciona cada chave ao respectivo valor, use for chave, valor in dados.items():.
A função obter usa uma consulta por chave e devolve o valor correspondente. Se a chave não existir, dados[chave] pode gerar KeyError; Mapping não promete que uma chave específica esteja presente.
Não anote como Mapping uma função que atribui, remove ou atualiza entradas. Essas operações não fazem parte do contrato de consulta.
Se a implementação abaixo deve modificar um dicionário, mantenha um contrato que permita essa escrita, como dict[str, int]. Não substitua por Mapping só para torná-la mais genérica.
def registrar_pontos(pontos: dict[str, int], nome: str) -> None:
pontos[nome] = pontos.get(nome, 0) + 1
placar = {"Ana": 3}
registrar_pontos(placar, "Ana")
print(placar) # {'Ana': 4}
Dica
Troque dict[K, V] por Mapping[K, V] somente se todas as operações da função forem de leitura. O tipo concreto usado por quem chama não decide o contrato; as operações da implementação decidem.
Qual assinatura é adequada para uma função que apenas devolve o valor associado a uma chave?

Passo 6 de 8
Identifique limites de contratos de coleção e iteração que precisam ser tratados ou documentados pela API.
Iterable[T] permite obter elementos por iteração, mas não promete que a fonte tenha tamanho conhecido, seja finita, possa recomeçar ou entregue os mesmos elementos em uma segunda passagem. Uma lista costuma permitir essas expectativas; um gerador, não.
Do mesmo modo, Sequence[T] não garante que exista algum elemento, e Mapping[K, V] não garante que uma chave específica esteja presente. Essas são precondições do problema, não consequências da anotação.

A anotação Iterable[int] aceita os dois objetos, embora apenas um possa ser percorrido novamente com os mesmos valores.
Atenção
O mypy verifica se as operações usadas são compatíveis com o contrato declarado. Ele não consegue concluir, apenas por Iterable[T], se a fonte será esgotada, infinita ou repetível.
Esta função usa duas passagens: uma para somar e outra para contar. Ambas são operações permitidas sobre Iterable[int], portanto a anotação é válida. Mas um iterador pode já estar esgotado quando a segunda passagem começa.
from collections.abc import Iterable
def resumo(valores: Iterable[int]) -> tuple[int, int]:
total = sum(valores)
quantidade = sum(1 for _ in valores)
return total, quantidade
print(resumo([2, 4, 6]))
print(resumo(numero for numero in [2, 4, 6]))
# Saída:
# (12, 3)
# (12, 0)Dica
Salve o código em um arquivo e execute python nome_do_arquivo.py. Depois rode python -m mypy nome_do_arquivo.py: a ausência de erro estático não transforma a segunda passagem em uma operação segura para todo Iterable.
Se total e quantidade bastam, calcule os dois no mesmo laço. Assim, a função continua aceitando listas, tuplas, conjuntos e geradores sem pressupor reinício.
Materializar com itens = list(valores) também permite múltiplas passagens, mas é uma decisão explícita: só faz sentido quando a fonte é finita e manter todos os elementos em memória é aceitável. Iterable não oferece essas garantias.
from collections.abc import Iterable
def resumo(valores: Iterable[int]) -> tuple[int, int]:
total = 0
quantidade = 0
for valor in valores:
total += valor
quantidade += 1
return total, quantidade
print(resumo(numero for numero in [2, 4, 6])) # (12, 3)Exemplo
Uma função que acessa dados[0] deve tratar ou declarar que a Sequence não pode estar vazia. Uma função que busca configuracao[chave] deve tratar KeyError ou declarar que a chave é obrigatória. E uma função que consome um iterável deve informar esse efeito se ele for relevante para quem chama.
Uma função anotada com Iterable[int] pode pressupor que duas iterações produzirão os mesmos números, porque listas permitem isso.
Compare a função resumo com lista e com gerador. Explique por que o mypy aceita as duas chamadas e escolha entre uma única passagem ou list(valores) para uma API que só precisa devolver total e quantidade.
Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8
Entenda o que Sequence e Mapping restringem na assinatura — e o que eles não mudam no objeto recebido.
Ao receber Sequence[T] ou Mapping[K, V], sua função declara as operações de consulta de que precisa. No código tipado, isso impede chamar métodos de alteração que não pertencem a esses contratos, como append em uma Sequence ou atribuição por chave em um Mapping.
Mas a anotação não transforma o argumento em uma cópia nem altera seu tipo concreto. Se quem chamou a função passou uma list ou um dict, esse objeto continua mutável por outras referências.
A função consulta pelo contrato; outra referência ainda pode alterar a mesma lista ou o mesmo dicionário.

A interface limita o que este trecho de código pode pedir ao objeto; ela não muda o objeto compartilhado.
Crie um arquivo e execute este código.
from collections.abc import Mapping, Sequence
def mostrar_primeiro(nomes: Sequence[str]) -> None:
print(f"Antes: {nomes[0]}")
nomes_originais[0] = "Bia"
print(f"Depois: {nomes[0]}")
def mostrar_status(status: Mapping[str, str]) -> None:
print(f"Antes: {status['pedido']}")
status_original['pedido'] = "enviado"
print(f"Depois: {status['pedido']}")
nomes_originais = ["Ana", "Caio"]
status_original = {"pedido": "aberto"}
mostrar_primeiro(nomes_originais)
mostrar_status(status_original)Exemplo
A saída é:
Antes: Ana
Depois: Bia
Antes: aberto
Depois: enviadoAs consultas posteriores enxergam as alterações porque nomes e nomes_originais referenciam a mesma lista; o mesmo ocorre com status e status_original.
Atenção
Sequence e Mapping não congelam coleções, não congelam objetos armazenados nelas e não validam automaticamente os valores em execução. A anotação orienta o verificador estático e as operações disponíveis pelo contrato.
Em um arquivo analisado pelo mypy, estas linhas são incompatíveis com os contratos dos parâmetros.
from collections.abc import Mapping, Sequence
def tentar_alterar(
nomes: Sequence[str],
status: Mapping[str, str],
) -> None:
nomes.append("Dora") # erro: Sequence não oferece append
status["pedido"] = "pago" # erro: Mapping não permite atribuição por chaveEsses erros não afirmam que uma lista nunca pode receber append ou que um dicionário nunca pode mudar. Eles afirmam que, por essa referência anotada como interface de consulta, a função não pode depender dessas operações.
Se a API precisa alterar a coleção, o contrato deve expressar essa necessidade. Se apenas consulta, Sequence ou Mapping deixa essa intenção explícita — sem prometer que o conteúdo ficará estável.
Se uma função recebe itens: Sequence[str] e a chamada passa uma lista, outra referência à mesma lista pode alterar seus elementos; uma consulta posterior feita pela função pode observar a mudança.
Em uma função com config: Mapping[str, int], a expressão config['tentativas'] = 3 deve ser aceita pelo mypy porque um dict pode ser passado para o parâmetro.

Passo 8 de 8
Aplique Iterable, Iterator, Sequence e Mapping em uma API curta, verifique o código localmente e registre as garantias que ainda dependem da implementação ou da documentação.
Leia a implementação antes de anotar a assinatura. A fonte de chaves será apenas percorrida: use Iterable[K]. O catálogo será consultado por chave: use Mapping[K, V]. A transformação produz valores sob demanda e permite next(): retorne Iterator[V]. Já a seleção usa len implícito no acesso e posição por índice: ela precisa de Sequence[T].
Não generalize o retorno por reflexo: escolha o que sua API pretende oferecer a quem chama.
Compare as operações de cada função com o contrato mínimo que as disponibiliza.

Cada seta representa uma operação usada pela implementação, não o tipo concreto de um exemplo de chamada.
No seu computador, crie um arquivo chamado contratos.py com o código completo abaixo. Ele inclui chamadas com lista, tupla e gerador; nenhuma delas deve exigir conversão prévia para list ou dict.
from collections.abc import Iterable, Iterator, Mapping, Sequence
def resolved_values[K, V](
keys: Iterable[K], catalog: Mapping[K, V]
) -> Iterator[V]:
"""Produz o valor do catálogo para cada chave recebida."""
for key in keys:
yield catalog[key]
def at[T](items: Sequence[T], index: int) -> T:
"""Devolve o elemento na posição solicitada."""
return items[index]
names: dict[int, str] = {1: "Ana", 2: "Bia", 3: "Caio"}
assert list(resolved_values([1, 3], names)) == ["Ana", "Caio"]
assert list(resolved_values((2, 1), names)) == ["Bia", "Ana"]
def key_stream() -> Iterator[int]:
yield 3
yield 2
produced = resolved_values(key_stream(), names)
assert next(produced) == "Caio"
assert list(produced) == ["Bia"]
assert at(["zero", "um"], 1) == "um"
assert at((10, 20, 30), 0) == 10
print("Casos executados com sucesso.")Dica
key_stream() é consumido conforme resolved_values avança. O retorno também é um iterador: depois de next(produced), resta apenas o próximo valor para list(produced).
No terminal, no diretório do arquivo, execute os dois comandos. O primeiro confirma o comportamento dos casos incluídos; o segundo verifica se as operações usadas respeitam as anotações.
python contratos.py
python -m mypy --strict --python-version 3.12 contratos.pyAtenção
catalog[key] pressupõe que toda chave produzida exista no mapeamento; caso contrário, Python pode lançar KeyError. Além disso, Iterable não promete uma segunda passagem, finitude ou tamanho conhecido. Sequence não garante que o índice seja válido, e Mapping não congela o dicionário concreto recebido.
Resumo
Use a menor interface que sustenta as operações reais e documente o que ela não assegura.
Iterable[T] serve para uma entrada apenas percorrida.Iterator[T] descreve produção sob demanda e avanço explícito com next.Sequence[T] é adequado quando há acesso por posição, sem exigir list.Mapping[K, V] expressa consulta por chave e valor, sem exigir dict nem prometer escrita.Depois de executar o arquivo e o mypy, informe: as quatro assinaturas finais, uma justificativa baseada nas operações usadas e pelo menos duas garantias que a tipagem não expressa. Mencione também se os casos com lista, tupla e gerador funcionaram.
Escreva pelo menos 240 caracteres (0/240).
Parabéns! Você concluiu: Escolher contratos de coleções e iteradores
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks