Trilha de aprendizado · Nível 12 · Tutorial 6

Escolher contratos de coleções e iteradores

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.

  • Nível: Avançado
  • Duração: 18 min
  • 8 passos
Escolher contratos de coleções e iteradores

O que você vai percorrer

  1. 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
  2. Receber elementos com Iterable Generalize parâmetros que apenas são percorridos, preservando o tipo dos elementos sem exigir list. 2 min
  3. 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
  4. 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
  5. Consultar por chave com Mapping Generalize parâmetros de consulta de dict para Mapping sem perder os tipos de chaves e valores. 2 min
  6. Reconhecer garantias que faltam Identifique limites de contratos de coleção e iteração que precisam ser tratados ou documentados pela API. 3 min
  7. 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
  8. 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

O que você vai aprender

  • Escolher entre Iterable, Iterator, Sequence e Mapping conforme as operações utilizadas.
  • Generalizar uma assinatura que exige list ou dict sem necessidade.
  • Anotar funções geradoras pelo tipo dos valores produzidos.
  • Identificar garantias que não são expressas por uma anotação de coleção ou iterável.

Antes de começar

  • Preservar relações de tipos em funções genéricas
  • Consumir iteradores com iter e next
  • Produzir valores sob demanda com yield

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.

O contrato vem das operações

Comece pelo corpo da função

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?”

Exemplo de chamada × requisito real

A representação concreta usada por quem chama e o contrato exigido pela função são decisões diferentes.

Diagrama comparando uma lista usada em uma chamada com as poucas operações que uma função realmente utiliza, destacando que o contrato deve ser guiado pelas operações.

A lista à esquerda é uma possibilidade de entrada; as operações no centro definem o contrato necessário.

Contratos já prontos

Use interfaces da biblioteca padrão

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.

Uma anotação concreta pode ser excessiva

A função só percorre os nomes; ela não usa nenhuma operação exclusiva de list.

python
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

Faça um inventário curto

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.

Entrada e saída têm decisões próprias

Não generalize o retorno por reflexo

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

A saída é uma promessa

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.

Diagnóstico de requisitos

O que cada observação indica?

Associe cada situação à conclusão correta sobre a anotação.

Toque em um item e depois no par correspondente.

Passo 2 de 8

Receber elementos com Iterable

Generalize parâmetros que apenas são percorridos, preservando o tipo dos elementos sem exigir list.

Quando só percorrer é suficiente

Um contrato para fornecer elementos

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.

Fontes diferentes, mesma operação

Todas estas fontes podem alimentar uma função que faz uma única passagem com for.

Diagrama mostrando lista, tupla, conjunto e fluxo de gerador convergindo para um laço for e produzindo uma saída comum.

Iterable[str] descreve a capacidade de fornecer strings durante a iteração, não o tipo concreto da fonte.

Refatore a assinatura, não o laço

De list para Iterable

A implementação já faz somente uma passagem. Portanto, apenas o contrato do parâmetro precisa mudar.

python
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

O que o contrato permite — e não permite

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.

Preserve o tipo genérico dos elementos

A generalização não apaga relações de tipo

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.

Um elemento do mesmo tipo

T continua ligando o tipo produzido ao tipo dos elementos fornecidos.

python
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 é str

Pratique a escolha do contrato

Complete a anotação

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

Identifique uma chamada compatível

Qual chamada é compatível com juntar_linhas(linhas: Iterable[str])?

Passo 3 de 8

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.

Quando o avanço faz parte do contrato

Iterator inclui avanço explícito

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.

Iterable ou Iterator?

Compare o que cada contrato disponibiliza diretamente para a implementação.

Diagrama comparando Iterable, que fornece iterador por iter(), com Iterator, que fornece iter(), next() e avança seu próprio estado.

Iterator[T] acrescenta o avanço com next() ao contrato de iteração.

Consumir a fonte recebida

Função que avança o argumento

O retorno preserva o tipo do próximo valor.

python
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))  # 20

Exemplo

Por que não Iterable aqui?

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

Retorno também comunica a promessa

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.

Anotar o que yield produz

Um gerador retorna um iterador

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.

Gerador simples

Cada execução de yield produz uma str; por isso, o retorno é Iterator[str].

python
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-20

Exemplo

Escolha no retorno

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.

Verificar os contratos

Contrato do consumidor

Qual anotação deve substituir ????

def ler_cabecalho(fonte: ???) -> str:
    return next(fonte)

Tipo produzido pelo gerador

Complete a anotação:

def pares(limite: int) -> Iterator[_____]:
    for numero in range(limite):
        if numero % 2 == 0:
            yield numero

Passo 4 de 8

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.

Quando a posição faz parte do contrato

Mais que percorrer elementos

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.

Operações garantidas por Sequence

Compare os contratos pelo que a função realmente faz.

Diagrama comparando uma sequência indexada, que permite iterar, medir comprimento e acessar posições, com um fluxo de elementos que permite apenas iteração.

Sequence[T] atende a iteração, comprimento e posições; uma fonte apenas iterável não promete essas operações.

Uma assinatura que aceita listas e tuplas

Selecionar extremos sem exigir list

A função usa len e índices; por isso, Sequence[T] é o contrato mínimo adequado.

python
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 tipo do elemento continua preservado

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

Critério de escolha

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.

Associe a operação ao contrato

Qual contrato atende à implementação?

Relacione cada comportamento ao contrato de entrada menos restritivo apropriado.

Toque em um item e depois no par correspondente.

Passo 5 de 8

Consultar por chave com Mapping

Generalize parâmetros de consulta de dict para Mapping sem perder os tipos de chaves e valores.

Consulta é um contrato diferente de alteração

Quando usar Mapping

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.

Operações de consulta em Mapping

Diagrama de um mapeamento com cartões de chaves ligados a cartões de valores; uma lupa consulta um valor por sua chave e outro fluxo percorre pares chave-valor, sem setas de alteração.

Mapping[K, V] modela leitura de chaves e valores, não escrita.

Preserve a relação entre chave e valor

Busca tipada por chave

A chave recebida tem o mesmo tipo K das chaves do mapeamento, e o resultado tem tipo V.

python
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

Iterar chaves não é o mesmo que consultar pares

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 esconda uma escrita sob um contrato de consulta

A alteração muda a escolha

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.

Escrita exige outro contrato

python
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

Critério prático

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.

Escolha o contrato compatível

Generalize sem prometer escrita

Qual assinatura é adequada para uma função que apenas devolve o valor associado a uma chave?

Passo 6 de 8

Reconhecer garantias que faltam

Identifique limites de contratos de coleção e iteração que precisam ser tratados ou documentados pela API.

O contrato não promete tudo

Operações permitidas não são garantias de conteúdo

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.

Duas passagens, duas realidades

Comparação entre uma lista reutilizável que alimenta duas passagens e um gerador que se esgota após a primeira passagem.

A anotação Iterable[int] aceita os dois objetos, embora apenas um possa ser percorrido novamente com os mesmos valores.

Atenção

Compatível não significa suficiente

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.

Uma segunda passagem pode falhar silenciosamente

Código aceito, resultado incorreto

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.

Compare lista e gerador

python
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

Experimente localmente

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.

Escolha a correção pelo requisito

Uma passagem é a opção mais geral

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.

Resumo em uma única passagem

python
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

Precondições também precisam aparecer no código ou na documentação

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.

Verifique o limite do contrato

Garantia de Iterable

Uma função anotada com Iterable[int] pode pressupor que duas iterações produzirão os mesmos números, porque listas permitem isso.

Decida a correção

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

Separar consulta de imutabilidade

Entenda o que Sequence e Mapping restringem na assinatura — e o que eles não mudam no objeto recebido.

Contrato de consulta não congela o objeto

O que a anotação restringe

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.

Duas referências, um só objeto

A função consulta pelo contrato; outra referência ainda pode alterar a mesma lista ou o mesmo dicionário.

Diagrama mostrando uma lista e um dicionário concretos no centro. Uma seta de uma função anotada com Sequence e Mapping aponta para consultas, enquanto outra seta externa aponta para operações de alteração nos mesmos objetos.

A interface limita o que este trecho de código pode pedir ao objeto; ela não muda o objeto compartilhado.

Observe a alteração pela outra referência

Mesmo objeto, consultas em momentos diferentes

Crie um arquivo e execute este código.

python
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

Resultado esperado

A saída é:

Antes: Ana
Depois: Bia
Antes: aberto
Depois: enviado

As 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

Não confunda contrato com proteção em tempo de execuçã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.

O que o verificador rejeita?

Operação fora da interface

Em um arquivo analisado pelo mypy, estas linhas são incompatíveis com os contratos dos parâmetros.

python
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 chave

A distinção essencial

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

Verifique a previsão

Contrato e objeto concreto

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.

Operação oferecida pelo contrato

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

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.

Decida pelo que o código faz

Critério final de escolha

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.

Operações determinam contratos

Compare as operações de cada função com o contrato mínimo que as disponibiliza.

Diagrama de duas funções: uma percorre chaves e consulta um catálogo, recebendo Iterable e Mapping e retornando Iterator; outra acessa uma posição numerada de uma Sequence.

Cada seta representa uma operação usada pela implementação, não o tipo concreto de um exemplo de chamada.

Prática local: implemente e execute

Crie o arquivo

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.

contratos.py

python
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

O que observar

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

Verifique duas dimensões

Execução e compatibilidade estática

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.

Comandos

bash
python contratos.py
python -m mypy --strict --python-version 3.12 contratos.py

Atenção

A tipagem não substitui precondições

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.

Revisão e justificativa final

Resumo

Checklist de contratos

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.
  • Compatibilidade estática não garante chaves presentes, índices válidos, reinício, finitude ou estabilidade do conteúdo.

Explique sua refatoração

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

Tutorial concluído

Parabéns! Você concluiu: Escolher contratos de coleções e iteradores

Muito bem! Ao revisar uma assinatura, pergunte primeiro quais operações o código precisa — e quais promessas o retorno deve fazer.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso