Trilha de aprendizado · Nível 12 · Tutorial 2

Anotar coleções e estruturas aninhadas

Expressar os tipos de elementos, chaves e valores de coleções, incluindo estruturas aninhadas, e nomear anotações extensas com aliases.

  • Nível: Intermediário
  • Duração: 18 min
  • 8 passos
Anotar coleções e estruturas aninhadas

O que você vai percorrer

  1. Especificar os elementos de listas e conjuntos Use tipos parametrizados para declarar o tipo de cada elemento aceito por listas e conjuntos. 2 min
  2. Separar os tipos de chaves e valores Anote dicionários especificando, separadamente, o tipo das chaves e o tipo dos valores. 2 min
  3. Distinguir posições fixas e comprimento variável em tuplas Escolha entre anotações de tuplas com posições definidas e tuplas homogêneas de tamanho variável. 2 min
  4. Declarar o contrato de coleções vazias Declare o tipo pretendido de listas, conjuntos e dicionários vazios para que o contrato continue claro antes das primeiras inserções. 2 min
  5. Compor anotações para estruturas aninhadas Combine anotações de coleções para descrever cada nível de uma estrutura e verificar o tipo esperado nas operações internas. 3 min
  6. Armazenar objetos de classes próprias Use o nome de uma classe para declarar quais instâncias uma coleção pode armazenar e preserve esse contrato em estruturas aninhadas. 2 min
  7. Dar nomes às anotações com aliases Use aliases para tornar contratos extensos mais legíveis, sem mudar os valores aceitos pela anotação. 2 min
  8. Aplicar e verificar os contratos de uma coleção Integre anotações de coleções em um script curto, use o mypy para localizar incompatibilidades e corrija o código preservando contratos específicos. 4 min

O que você vai aprender

  • Anotar listas, conjuntos e dicionários com os tipos de seus componentes.
  • Distinguir anotações de tuplas com posições fixas das que representam comprimento variável.
  • Representar coleções aninhadas e coleções de objetos próprios.
  • Criar aliases de tipos e verificar inserções ou retornos incompatíveis.

Antes de começar

  • Verificar anotações com mypy
  • Listas, tuplas, dicionários, conjuntos e estruturas aninhadas.
  • Criar objetos com classes e métodos de instância

Passo 1 de 8

Especificar os elementos de listas e conjuntos

Use tipos parametrizados para declarar o tipo de cada elemento aceito por listas e conjuntos.

O contêiner e seus elementos

Um tipo para o conteúdo

Em uma anotação como list[int], list identifica o contêiner e int define o tipo de cada elemento que ele deve guardar. Para conjuntos, a mesma ideia aparece em set[str].

Use os tipos embutidos em minúsculas: não é necessário importar List ou Set de typing.

Leia de fora para dentro

A forma externa mostra a coleção; o tipo entre colchetes descreve seus itens.

Comparação visual entre uma lista contendo números inteiros com a anotação list[int] e um conjunto contendo palavras com a anotação set[str].

list e set descrevem o contêiner; int e str descrevem cada elemento.

O contrato vale na criação e na alteração

Listas e conjuntos anotados

Cada valor inicial e cada novo argumento devem ser compatíveis com o tipo entre colchetes.

python
idades: list[int] = [18, 27, 42]
idades.append(35)      # compatível
# idades.append("30")  # incompatível para mypy

etiquetas: set[str] = {"novo", "oferta"}
etiquetas.add("online")      # compatível
# etiquetas.add(10)           # incompatível para mypy

Dica

Anotação não é conversão

idades: list[int] não transforma automaticamente textos em números e não bloqueia uma inserção enquanto o programa roda. O contrato permite que o mypy identifique a incompatibilidade ao analisar o código.

Pratique o contrato de cada elemento

Complete a anotação

Uma lista que deve conter somente nomes precisa da anotação nomes: list[____] = ["Ana", "Caio"].

Complete o conjunto

Para guardar somente códigos numéricos únicos, escreva codigos: set[____] = {101, 205}.

Identifique a inserção incompatível

Qual linha o mypy deve sinalizar?

Considere pontuacoes: list[int] = [10, 20]. Qual inserção viola o contrato?

Passo 2 de 8

Separar os tipos de chaves e valores

Anote dicionários especificando, separadamente, o tipo das chaves e o tipo dos valores.

Dois contratos em um dicionário

Chaves primeiro, valores depois

A anotação de um dicionário tem dois parâmetros: dict[tipo_da_chave, tipo_do_valor].

Em dict[str, int], toda chave deve ser str e todo valor deve ser int. A posição importa: o primeiro tipo descreve o que fica antes dos dois-pontos; o segundo, o que fica depois.

Lendo cada par

Observe como os lados de cada par correspondem à anotação.

Diagrama de pares de dicionário: chaves textuais à esquerda ligadas a valores inteiros à direita, com a estrutura externa representando dict de string para inteiro.

Em cada par chave: valor, a chave atende ao primeiro parâmetro e o valor ao segundo.

Criar e atualizar dentro do contrato

Um dicionário de pontuações

Os pares iniciais e as atribuições posteriores precisam respeitar os dois tipos declarados.

python
pontuacoes: dict[str, int] = {
    "Ana": 18,
    "Bia": 24,
}

pontuacoes["Caio"] = 31       # chave str, valor int
pontuacoes["Ana"] = 20        # atualiza com outro int

# pontuacoes[7] = 12           # erro: a chave deveria ser str
# pontuacoes["Davi"] = "dez" # erro: o valor deveria ser int

Dica

Contrato estático

A anotação não converte nem bloqueia valores enquanto o programa executa. Ao verificar o arquivo, o mypy pode apontar tanto uma chave incompatível quanto um valor incompatível.

Associe cada contrato

Chaves e valores

Relacione cada conjunto de pares à anotação de dicionário que o descreve.

Toque em um item e depois no par correspondente.

Passo 3 de 8

Distinguir posições fixas e comprimento variável em tuplas

Escolha entre anotações de tuplas com posições definidas e tuplas homogêneas de tamanho variável.

Dois contratos para tuplas

Posições fixas

Em uma tupla de posições fixas, cada tipo descreve uma posição específica. A anotação tuple[str, int] exige exatamente dois itens: primeiro um str, depois um int.

Assim, ("Ana", 3) atende ao contrato. Já (3, "Ana") troca a ordem, e ("Ana", 3, 8) tem um item a mais.

Ordem e quantidade fazem parte do contrato

Compare a tupla de duas posições com uma sequência homogênea repetível.

Diagrama lado a lado: uma tupla com dois espaços fixos, o primeiro contendo texto e o segundo contendo número inteiro; outra tupla com vários espaços repetidos, todos contendo texto, e reticências ao final.

Em tuple[str, int], posição, tipo e quantidade são definidos. Em tuple[str, ...], o tipo se repete e a quantidade pode variar.

Quando o comprimento pode variar

Reticências significam repetição

Use tuple[str, ...] quando a tupla puder ter qualquer quantidade de textos — inclusive nenhum. Todos os itens, se existirem, devem ser str.

Não confunda as duas formas:

  • tuple[str]: exatamente uma posição, do tipo str.
  • tuple[str, ...]: zero ou mais posições, todas do tipo str.

Exemplos válidos e inválidos

python
par: tuple[str, int] = ("maçãs", 4)
# par = (4, "maçãs")       # incompatível: ordem invertida
# par = ("maçãs", 4, 2)     # incompatível: três itens

etiqueta: tuple[str] = ("novo",)
# etiqueta = ()              # incompatível: falta a única posição

rotulos: tuple[str, ...] = ()
rotulos = ("novo", "oferta", "online")
# rotulos = ("novo", 7)     # incompatível: 7 não é str

Escolha a anotação

Códigos de cupom

Uma função recebe códigos de cupom. Pode receber nenhum código, um código ou vários, e todos são textos. Qual é a anotação adequada?

Cheque o contrato por posição

Par nome–pontuação

Considere resultado: tuple[str, int]. Qual valor é compatível?

Passo 4 de 8

Declarar o contrato de coleções vazias

Declare o tipo pretendido de listas, conjuntos e dicionários vazios para que o contrato continue claro antes das primeiras inserções.

Um vazio ainda pode ter contrato

Declare o que virá depois

Uma coleção vazia não mostra quais valores ela deverá aceitar no restante do programa. Quando esse contrato importa, escreva uma anotação explícita na criação.

A anotação descreve os valores permitidos no futuro; ela não depende de haver elementos agora.

Coleção vazia, contrato definido

Diagrama com uma lista, um conjunto e um dicionário inicialmente vazios, ligados respectivamente a valores de texto, números inteiros e pares texto-número.

Mesmo vazias, as coleções podem declarar o tipo de elementos — ou de chaves e valores — que receberão.

Anotações na criação

Cada variável recebe o contrato que suas operações futuras deverão respeitar.

python
nomes: list[str] = []
codigos: set[int] = set()
pontos_por_usuario: dict[str, int] = {}

O contrato orienta as inserções

Exemplo

Valores iniciais não são a única referência

tarefas: list[str] = [] começa sem valores, mas já informa que append deve receber textos.

tarefas: list[str] = []
tarefas.append("revisar pedido")  # compatível
tarefas.append(3)                  # incompatível para o mypy

A anotação não converte 3 em texto nem bloqueia essa linha durante a execução. Ela permite que o verificador aponte o conflito estático.

Dica

Não use Any como atalho

Se você sabe que a coleção guardará textos, inteiros ou pares específicos, declare esse tipo. Trocar o contrato por Any elimina justamente a verificação que ajudaria a detectar uma inserção errada.

Prática: complete o contrato

Lista de participantes

Complete a anotação para que a lista aceite nomes, mesmo antes de receber o primeiro nome:

participantes: ____ = []

Digite apenas a anotação da coleção.

Passo 5 de 8

Compor anotações para estruturas aninhadas

Combine anotações de coleções para descrever cada nível de uma estrutura e verificar o tipo esperado nas operações internas.

Uma anotação para cada nível

Leia de fora para dentro

Uma coleção aninhada usa uma anotação de coleção dentro de outra. Em dict[str, list[int]], a camada externa é um dicionário: cada chave é str e cada valor é uma list[int].

Ao acessar vendas["segunda"], o resultado deixa de ser o dicionário inteiro: é uma lista de inteiros.

Camadas da estrutura

Cada nível de dados corresponde a uma camada da anotação.

Diagrama de um dicionário cujas chaves de texto apontam para listas contendo números inteiros, conectado visualmente à estrutura de colchetes dict[str, list[int]].

A camada externa descreve chaves e valores; a camada interna descreve os elementos de cada lista.

Acesso e inserção obedecem à camada interna

Dicionário de listas

Observe os tipos obtidos em cada acesso.

python
vendas: dict[str, list[int]] = {
    "segunda": [12, 18],
    "terca": [9],
}

valores_da_segunda = vendas["segunda"]  # list[int]
primeira_venda = vendas["segunda"][0]   # int

vendas["terca"].append(15)  # correto: a lista interna aceita int
# vendas["terca"].append("15")  # mypy aponta incompatibilidade

Exemplo

O contêiner externo não basta

A chave "terca" é compatível com str, mas isso não torna qualquer valor válido no acesso interno. Como vendas["terca"] é list[int], o argumento de append precisa ser int.

Listas de tuplas

Composição com posições fixas

Você também pode colocar uma tupla tipada dentro de uma lista. Em list[tuple[str, int]], cada elemento da lista é uma tupla de exatamente duas posições: primeiro um texto, depois um inteiro.

Itens com nome e quantidade

A anotação acompanha a estrutura real dos dados.

python
itens: list[tuple[str, int]] = [
    ("caderno", 3),
    ("caneta", 10),
]

primeiro_item = itens[0]       # tuple[str, int]
nome = itens[0][0]             # str
quantidade = itens[0][1]       # int

itens.append(("borracha", 5))  # correto
# itens.append(("régua", "5"))  # incompatível: a segunda posição deve ser int

Pratique a leitura das camadas

Complete a anotação

Uma agenda guarda, para cada dia em texto, uma lista de horários inteiros: agenda: ___ = {"segunda": [9, 14]}

Insira no nível correto

Considerando agenda: dict[str, list[int]], qual operação é compatível com a anotação?

Passo 6 de 8

Armazenar objetos de classes próprias

Use o nome de uma classe para declarar quais instâncias uma coleção pode armazenar e preserve esse contrato em estruturas aninhadas.

A classe descreve cada elemento

Instâncias, não textos

Quando uma lista deve guardar objetos de uma classe, use o nome da classe como o tipo do elemento. Em list[Produto], a coleção aceita instâncias de Produto.

A anotação não representa a classe em si: ela descreve os objetos criados a partir dela que serão armazenados.

Coleção de instâncias

Cada bloco interno representa uma instância de Produto; a lista externa é anotada como list[Produto].

Diagrama de uma lista contendo três objetos de produto, cada um com atributos nome e preco, ligada à anotação conceitual list de Produto.

O tipo entre colchetes é o tipo de cada elemento da coleção.

Compor o contrato e usar o objeto

Catálogo por categoria

A chave externa é um texto, e cada valor é uma lista de instâncias de Produto.

python
class Produto:
    def __init__(self, nome: str, preco: float) -> None:
        self.nome = nome
        self.preco = preco

    def esta_em_promocao(self) -> bool:
        return self.preco < 50.0

catalogo: dict[str, list[Produto]] = {
    "papelaria": [Produto("Caderno", 32.0)]
}

produto = catalogo["papelaria"][0]
print(produto.nome)
print(produto.esta_em_promocao())

Tipo preservado no acesso

Leia a anotação de fora para dentro: catalogo["papelaria"] produz list[Produto]; adicionar [0] produz Produto. Por isso, o verificador conhece nome e esta_em_promocao() nesse ponto.

Já catalogo["papelaria"].append("Caneta") é incompatível: uma string não é uma instância de Produto. A anotação orienta o mypy, mas não bloqueia essa chamada automaticamente durante a execução.

Dica

Ao verificar

Com o arquivo salvo, execute python -m mypy nome_do_arquivo.py. Um diagnóstico nessa inserção indica que o valor fornecido não atende ao contrato Produto da lista interna.

Escolha o contrato correto

Anotação do estoque

Considere a classe e a variável abaixo:

class Produto:
    def __init__(self, nome: str) -> None:
        self.nome = nome

estoque = [Produto("Caderno"), Produto("Caneta")]

Qual anotação declara corretamente o contrato de estoque?

Passo 7 de 8

Dar nomes às anotações com aliases

Use aliases para tornar contratos extensos mais legíveis, sem mudar os valores aceitos pela anotação.

Um nome para uma estrutura extensa

Alias de tipo com Python 3.12

Quando uma anotação aninhada aparece em vários pontos, dê a ela um nome significativo com a instrução type. Em Python 3.12+, um alias para um estoque pode ser declarado assim: type Estoque = dict[str, list[Produto]].

Leia Estoque como uma abreviação do contrato completo: cada chave é str e cada valor é uma lista de instâncias de Produto.

O alias aponta para o mesmo contrato

Diagrama mostrando o nome Estoque apontando para um dicionário cujas chaves são textos e cujos valores são listas de objetos Produto.

O nome curto substitui a repetição visual, mas preserva todos os níveis da estrutura.

Reutilize o contrato

Alias em variável, parâmetro e retorno

Considere que a classe Produto já foi definida.

python
class Produto:
    def __init__(self, nome: str) -> None:
        self.nome = nome


type Estoque = dict[str, list[Produto]]

catalogo: Estoque = {
    "frutas": [Produto("maçã")]
}

def adicionar_categoria(estoque: Estoque, categoria: str) -> Estoque:
    estoque[categoria] = []
    return estoque

Mesmo contrato, menos repetição

O alias pode anotar variáveis, parâmetros e retornos. Ele não cria uma classe chamada Estoque nem um novo tipo nominal: para o mypy, Estoque equivale a dict[str, list[Produto]].

Por isso, o valor atribuído ainda precisa respeitar a estrutura completa.

Pratique a declaração

Complete o alias

Complete a linha para nomear a estrutura dict[str, list[Produto]] como Estoque:

_____ Estoque = dict[str, list[Produto]]

O que o alias não faz

Contrato estático

Verdadeiro ou falso: depois de declarar type Estoque = dict[str, list[Produto]], Python converte automaticamente textos inseridos nas listas em objetos Produto.

Passo 8 de 8

Aplicar e verificar os contratos de uma coleção

Integre anotações de coleções em um script curto, use o mypy para localizar incompatibilidades e corrija o código preservando contratos específicos.

Leia a estrutura antes de verificar

Um contrato por nível

Neste programa, o dicionário externo associa uma categoria a uma lista de objetos Produto. Além dele, haverá um conjunto de textos e dois formatos de tupla: um resumo com posições fixas e etiquetas de tamanho variável.

Ao investigar um diagnóstico, percorra a anotação de fora para dentro: descubra qual operação está sendo feita e qual é o tipo exigido exatamente naquele nível.

Mapa dos contratos usados

A imagem conecta cada parte do programa ao seu contrato estático.

Diagrama de um dicionário cujas chaves de texto apontam para listas de objetos Produto, acompanhado de um conjunto de textos, uma tupla fixa texto e inteiro, e uma tupla variável de textos.

Em dict[str, list[Produto]], a chave é str, o valor é uma list e cada elemento dessa lista deve ser um Produto.

Monte o script com três incompatibilidades

Prática no seu computador

Crie um arquivo chamado inventario.py no seu editor e copie o código completo abaixo. As três linhas marcadas com ERRO INTENCIONAL violam contratos diferentes: uma inserção em conjunto, uma atualização do dicionário aninhado e um retorno de tupla.

O alias usa a instrução type, disponível no Python 3.12 ou superior.

inventario.py

python
from dataclasses import dataclass


@dataclass
class Produto:
    nome: str
    preco_centavos: int


type Estoque = dict[str, list[Produto]]
type Resumo = tuple[str, int]


def adicionar(estoque: Estoque, categoria: str, produto: Produto) -> None:
    estoque.setdefault(categoria, []).append(produto)


def resumir(estoque: Estoque) -> Resumo:
    total = sum(
        produto.preco_centavos
        for produtos in estoque.values()
        for produto in produtos
    )
    return ("total", str(total))  # ERRO INTENCIONAL


estoque: Estoque = {}
categorias: set[str] = set()
etiquetas: tuple[str, ...] = ("novo", "disponivel")

adicionar(estoque, "livros", Produto("Python prático", 5990))
categorias.add("livros")
categorias.add(99)  # ERRO INTENCIONAL
estoque["ofertas"] = ["brinde"]  # ERRO INTENCIONAL

print(resumir(estoque))
print(etiquetas)

Verifique e corrija sem enfraquecer o contrato

Use o mypy como guia

No terminal, na pasta do arquivo, execute o comando abaixo. Corrija somente os valores ou a implementação: não use Any e não altere os aliases para tipos mais amplos.

Para cada erro, compare o tipo encontrado com o tipo exigido pela operação:

  1. set[str].add(...) exige um str.
  2. Cada valor de Estoque deve ser uma list[Produto].
  3. Resumo exige exatamente str na primeira posição e int na segunda.

Comando de verificação

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

Relate sua correção

Depois de ajustar o arquivo e executar o comando novamente, relate: qual tipo era esperado em cada um dos três locais, qual valor incompatível foi encontrado e qual foi o resultado da nova verificação.

Escreva pelo menos 120 caracteres (0/120).

Consolide o procedimento

Exemplo

As três correções essenciais

categorias.add("ofertas")
estoque["ofertas"] = [Produto("Marcador", 300)]
return ("total", total)

A primeira correção respeita set[str]; a segunda respeita o valor list[Produto] de Estoque; a terceira respeita as duas posições de Resumo.

Resumo

Checklist para coleções anotadas

Use este roteiro ao escrever ou revisar coleções tipadas:

  • Identifique o contêiner e o contrato de cada componente: elementos, chaves, valores ou posições.
  • Componha as anotações conforme a estrutura se aprofunda, como em dict[str, list[Produto]].
  • Anote coleções vazias quando elas não revelam sozinhas o contrato pretendido.
  • Use um alias com type para dar um nome reutilizável a uma estrutura extensa; ele não cria uma nova classe.
  • Diante do mypy, compare o tipo esperado ao tipo encontrado e corrija a origem da incompatibilidade sem recorrer a Any.

Tutorial concluído

Parabéns! Você concluiu: Anotar coleções e estruturas aninhadas

Muito bem! Agora você consegue ler uma estrutura aninhada, expressar seu contrato e usar diagnósticos estáticos para manter inserções, atualizações e retornos consistentes.

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