
Passo 1 de 8
Identificar a relação que a assinatura precisa preservar
Compare assinaturas que aceitam os mesmos valores, mas oferecem níveis diferentes de informação estática sobre o retorno.
Trilha de aprendizado · Nível 12 · Tutorial 5
Definir funções genéricas que preservem a relação entre os tipos recebidos e devolvidos, sem perder essa informação em object, Any ou uniões desnecessárias.
Identificar a relação que a assinatura precisa preservar
Compare assinaturas que aceitam os mesmos valores, mas oferecem níveis diferentes de informação estática sobre o retorno. 2 min
Declarar parâmetros de tipo e acompanhar a inferência
Use a sintaxe do Python 3.12 para declarar uma função genérica e acompanhe como o verificador preserva o tipo conhecido em cada chamada. 3 min
Relacionar o elemento de uma coleção ao resultado
Anote funções de seleção para que o tipo estático de cada elemento da lista seja preservado no valor retornado. 3 min
Respeitar o contrato no corpo genérico
Verifique se o corpo de uma função genérica usa apenas operações garantidas para qualquer tipo que possa ocupar T. 2 min
Separar os tipos de entrada e saída de uma transformação
Anote uma função que transforma cada elemento de uma lista, preservando a relação entre o tipo recebido pelo callback e o tipo produzido. 3 min
Usar um limite superior sem perder o subtipo
Use um limite superior para acessar operações de uma classe base sem apagar o subtipo conhecido em cada chamada. 3 min
Decidir quando um genérico agrega informação
Escolha a assinatura que comunica a relação real entre entradas e saída, sem introduzir parâmetros de tipo que não preservam informação útil. 2 min
Aplicar e verificar as relações de ponta a ponta
Integre seleção, transformação e preservação de subtipos em um único arquivo e confirme a inferência com mypy. 4 min

Passo 1 de 8
Compare assinaturas que aceitam os mesmos valores, mas oferecem níveis diferentes de informação estática sobre o retorno.
Considere uma função cujo comportamento é simplesmente devolver o valor que recebeu. Ela aceita vários tipos, mas o ponto importante não é apenas quais tipos entram: é que o tipo que sai deve acompanhar o tipo que entrou em cada chamada.
Uma assinatura ampla pode aceitar corretamente str e int, mas ainda apagar essa ligação para quem chama a função.
Cada chamada tem uma correspondência própria entre entrada e retorno.

À esquerda, texto retorna texto e número retorna número. À direita, ambos os resultados viram um retorno amplo para o verificador.
As três funções devolvem o argumento recebido, mas suas anotações informam coisas diferentes ao verificador.
def identidade_object(valor: object) -> object:
return valor
def identidade_any(valor: object) -> Any:
return valor
def identidade_uniao(valor: str | int) -> str | int:
return valor
nome = identidade_object("Ana") # tipo estático: object
codigo = identidade_any("A-17") # tipo estático: Any
idade = identidade_uniao(42) # tipo estático: str | intobject aceita valores de qualquer tipo, mas o retorno fica conhecido apenas como object. Any vai além: desliga a verificação nas operações posteriores.
A união parece mais específica, porém str | int no retorno só diz que uma das alternativas será devolvida. Ela não registra a regra: “se entrou str, sai str; se entrou int, sai int”. Assim, mesmo ao passar 42, o resultado continua sendo tratado como str | int.
Dica
Ao ler uma função, pergunte: o tipo do retorno depende do tipo de algum argumento nesta chamada? Se depende, listar alternativas aceitas pode não bastar; o contrato precisa representar a dependência.
Uma função tem a assinatura def eco(valor: str | int) -> str | int. Ela apenas retorna valor. Qual afirmação é correta?
Para uma função de identidade, o contrato desejado não é “aceita texto ou número”. É “para qualquer tipo escolhido nesta chamada, devolve esse mesmo tipo”.
Um parâmetro de tipo representa essa variável de relação: ele será determinado conforme o tipo estático disponível em cada uso da função. Na próxima etapa, você vai declarar essa relação na sintaxe do Python 3.12.
Uma união repetida na entrada e no retorno garante que o mesmo membro da união será devolvido?
Não. Ela permite alternativas em cada posição, mas não registra a correlação por chamada. Para preservar essa relação, será necessário um parâmetro de tipo.

Passo 2 de 8
Use a sintaxe do Python 3.12 para declarar uma função genérica e acompanhe como o verificador preserva o tipo conhecido em cada chamada.
Quando uma função recebe um valor e o devolve sem transformá-lo, declare um parâmetro de tipo entre colchetes após o nome da função. Em def identidade[T](valor: T) -> T:, o mesmo T liga o argumento ao retorno.
Esse T existe no escopo da função identidade: ele não é um tipo concreto fixado uma única vez no arquivo.
def identidade[T](valor: T) -> T:
return valor

Em cada chamada, o verificador infere um T compatível com o valor estático fornecido e o reutiliza no retorno.
Você chama a função normalmente. Não escreva identidade[str](...): o verificador infere T a partir do argumento disponível naquela chamada.
A inferência preserva o tipo estático que o código já conhece. Ela não inspeciona o objeto em execução para descobrir uma classe mais específica do que a anotação da variável.
Execute este arquivo com Python 3.12 e verifique-o com python -m mypy nome_do_arquivo.py.
def identidade[T](valor: T) -> T:
return valor
texto = identidade("ativo")
numero = identidade(42)
reveal_type(texto) # mypy: builtins.str
reveal_type(numero) # mypy: builtins.int
class Conta:
pass
class ContaPremium(Conta):
pass
conta: Conta = ContaPremium()
resultado = identidade(conta)
reveal_type(resultado) # mypy: Conta, não ContaPremium
Dica
reveal_type é apenas uma consulta ao mypy. Remova essas chamadas antes de executar o arquivo normalmente, pois elas não fazem parte da lógica da aplicação.
Complete a anotação: def eco[T](mensagem: T) -> ___:
No seu editor, crie um arquivo com a função abaixo. Acrescente duas chamadas: uma com uma lista de inteiros e outra com uma tupla de textos. Depois, ponha reveal_type em cada resultado e execute o mypy. A função deve continuar usando apenas T.
def primeiro[T](valor: T) -> T:
return valor
Quais tipos o mypy revelou para os dois resultados? Explique em uma frase por que eles podem ser diferentes embora chamem a mesma função.
Escreva pelo menos 30 caracteres (0/30).

Passo 3 de 8
Anote funções de seleção para que o tipo estático de cada elemento da lista seja preservado no valor retornado.
Uma função que escolhe um item de uma lista recebe uma coleção, mas devolve um elemento dela. Por isso, a relação correta é list[T] → T: o mesmo T representa o tipo dos elementos na entrada e o tipo do resultado.
Não use list[T] → list[T] quando a função retorna apenas um item. Isso prometeria uma coleção como resultado.

A lista contém valores de T; a seleção devolve um único valor do mesmo T.
Python 3.12 ou superior
def primeiro_ou_erro[T](itens: list[T]) -> T:
if not itens:
raise ValueError("a lista não pode estar vazia")
return itens[0]
nomes: list[str] = ["Ana", "Bia"]
primeiro_nome = primeiro_ou_erro(nomes) # str
valores: list[int | str] = [10, "dez"]
primeiro_valor = primeiro_ou_erro(valores) # int | strEm cada chamada, o verificador infere T a partir do tipo conhecido da lista. Com list[str], o retorno é str. Se os elementos já são int | str, o retorno continua sendo int | str; a função não escolhe uma alternativa mais específica por observar o objeto em execução.
Repetir T pede compatibilidade com o tipo inferido. Se houver mais de uma fonte de inferência na chamada, o verificador pode chegar a um tipo mais amplo que satisfaça todas elas.
Dica
A exceção encerra o caminho sem devolver um valor. Assim, todo caminho que retorna normalmente devolve um T real: itens[0].
Complete a anotação de retorno:
def ultimo_ou_erro[T](itens: list[T]) -> ____:
Por que esta implementação viola o contrato?
def primeiro[T](itens: list[T]) -> T: if not itens: return False return itens[0]

Passo 4 de 8
Verifique se o corpo de uma função genérica usa apenas operações garantidas para qualquer tipo que possa ocupar T.
Em uma função declarada com def identidade[T](valor: T) -> T, T não significa “o tipo dos exemplos que testei”. Ele pode representar qualquer tipo inferido em uma chamada. Por isso, o corpo precisa ser válido para todos esses tipos possíveis.
É seguro receber, armazenar, encaminhar e devolver um valor de T. Já chamar um método exclusivo de str ou somar um número pressupõe um contrato que a assinatura ainda não forneceu.
Compare operações que apenas transportam o valor com operações que dependem de uma característica específica dele.

T preserva a identidade do tipo, mas não promete métodos ou operadores específicos.
Dica
Pergunte: “Esta operação funciona para qualquer valor que possa ser T?” Se a resposta for não, ajuste a implementação ou declare um contrato que realmente garanta a operação — sem usar Any para esconder o diagnóstico.
Uma operação pode ser válida na coleção e inválida em cada elemento. Por exemplo, valores.append(novo) é uma operação da lista; isso não autoriza usar .append() em um valor individual de tipo T.
Exemplos para salvar em um arquivo Python 3.12 e verificar com mypy.
def repetir[T](valor: T) -> tuple[T, T]:
# Apenas recebe e devolve T: válido para qualquer T.
return valor, valor
def primeiro[T](valores: list[T]) -> T:
# A indexação é uma operação da lista; o resultado é T.
return valores[0]
def normalizar_incorreto[T](valor: T) -> T:
# Erro estático: T não garante o método lower().
return valor.lower()
def adicionar_incorreto[T](valor: T) -> T:
# Erro estático: T não garante suporte a + com int.
return valor + 1
def incluir[T](valores: list[T], novo: T) -> None:
# Válido: append pertence a list e novo tem o mesmo T dos elementos.
valores.append(novo)
# Não faça isto para calar o verificador:
# def normalizar[T](valor: T) -> T:
# return Any(valor).lower()Atenção
Se o mypy aponta que uma operação não existe em T, o problema é a promessa da assinatura ou a lógica genérica. Trocar o valor por Any elimina a verificação justamente onde o contrato precisava proteger você.
Relacione cada ação à garantia que torna seu uso válido no corpo de uma função.
Toque em um item e depois no par correspondente.
Por que uma função def limpar[T](valor: T) -> T: return valor.lower() é genericamente inválida, mesmo que limpar(" oi ") rode sem erro?
Escreva pelo menos 60 caracteres (0/60).

Passo 5 de 8
Anote uma função que transforma cada elemento de uma lista, preservando a relação entre o tipo recebido pelo callback e o tipo produzido.
Em uma seleção, o elemento de entrada e o resultado podiam compartilhar o mesmo parâmetro de tipo. Já uma transformação pode mudar o tipo: por exemplo, converter uma lista de int em uma lista de str.
Use dois parâmetros independentes: T é o tipo de cada elemento fornecido pela lista, e R é o tipo retornado pelo callback. A assinatura conecta os três pontos: list[T], Callable[[T], R] e list[R].

O callback consome um T por vez e produz um R; por isso, a lista resultante é list[R].
Crie um arquivo transformar.py com este código e execute python -m mypy transformar.py no seu ambiente.
from collections.abc import Callable
def mapear[T, R](
valores: list[T],
transformar: Callable[[T], R],
) -> list[R]:
resultado: list[R] = []
for valor in valores:
resultado.append(transformar(valor))
return resultado
def rotulo(numero: int) -> str:
return f"item-{numero}"
numeros: list[int] = [2, 4, 6]
rotulos = mapear(numeros, rotulo)
reveal_type(rotulos) # mypy: builtins.list[builtins.str]Na chamada mapear(numeros, rotulo), a lista informa T = int. A assinatura de rotulo informa que ela recebe int e devolve str, portanto R = str. Logo, rotulos é inferida como list[str].
Não especialize a função na chamada com colchetes: a inferência usa a coleção e o callback. Remova reveal_type antes de executar o arquivo normalmente.
Dica
A função sempre fornecerá elementos T ao callback. Portanto, o callback precisa aceitar o tipo dos elementos da lista. Com list[int], uma função que exige str não serve, mesmo que ela também retorne str.
Complete o retorno:
def mapear[T, R](valores: list[T], transformar: Callable[[T], R]) -> ____:
...from collections.abc import Callable
def mapear[T, R](
valores: list[T],
transformar: Callable[[T], R],
) -> list[R]:
return [transformar(valor) for valor in valores]
def deixar_maiusculo(texto: str) -> str:
return texto.upper()
numeros: list[int] = [1, 2, 3]
resultado = mapear(numeros, deixar_maiusculo)Execute o código no seu computador com python -m mypy transformar.py e explique por que deixar_maiusculo é incompatível com essa chamada de mapear.
Escreva pelo menos 40 caracteres (0/40).

Passo 6 de 8
Use um limite superior para acessar operações de uma classe base sem apagar o subtipo conhecido em cada chamada.
Um parâmetro de tipo irrestrito poderia representar qualquer tipo, então seu corpo não pode pressupor métodos específicos. Quando a função precisa usar uma operação garantida por uma classe base, declare um limite superior:
def normalizar[T: Documento](item: T) -> T:
Aqui, T pode ser Documento ou qualquer subtipo compatível. Dentro da função, o verificador sabe que todo T possui as operações declaradas em Documento.
O limite permite acessar o contrato comum, mas T mantém a identidade estática do subtipo recebido.

T: Documento permite usar membros de Documento; o retorno T conserva o subtipo inferido na chamada.
Exemplo em Python 3.12: a operação usada está no contrato da classe base, e o objeto devolvido continua sendo T.
class Documento:
def marcar_revisado(self) -> None:
print("Revisado")
class Fatura(Documento):
def calcular_imposto(self) -> float:
return 42.0
class Relatorio(Documento):
def exportar_pdf(self) -> None:
print("PDF exportado")
def revisar[T: Documento](documento: T) -> T:
documento.marcar_revisado()
return documento
fatura = revisar(Fatura())
revelar_imposto = fatura.calcular_imposto()
relatorio = revisar(Relatorio())
relatorio.exportar_pdf()Dica
O limite T: Documento justifica documento.marcar_revisado(). Repetir T no retorno diz que a função devolve o mesmo tipo estático que recebeu: Fatura continua sendo Fatura, e Relatorio continua sendo Relatorio.
O que a assinatura abaixo comunica?
def revisar[T: Documento](documento: T) -> T:
Atenção
Esta implementação viola o contrato:
def criar_revisado[T: Documento](documento: T) -> T:
return Documento()Se a chamada receber uma Fatura, o retorno prometido é Fatura; uma instância nova de Documento não satisfaz essa promessa. O limite afirma que T é compatível com Documento, não que todo Documento seja um T específico.
A anotação é analisada pelo verificador de tipos. Ela não adiciona uma checagem automática quando o programa roda, nem impede que dados externos incorretos cheguem ao código. Validações necessárias em tempo de execução continuam sendo responsabilidade explícita do programa.
Considere:
def revisar[T: Documento](documento: T) -> T:
documento.marcar_revisado()
return documentoPor que T: Documento é justificável? Por que o retorno é T em vez de Documento?
Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8
Escolha a assinatura que comunica a relação real entre entradas e saída, sem introduzir parâmetros de tipo que não preservam informação útil.
Um parâmetro de tipo é útil quando conecta posições da assinatura. Ele pode dizer, por exemplo: “o retorno tem o mesmo tipo da entrada” (T → T) ou “o callback transforma T em R”.
Se essa relação não existe, um genérico pode só tornar a assinatura mais abstrata, sem informar nada novo ao chamador.
As três assinaturas podem aceitar valores variados, mas prometem coisas diferentes sobre o resultado.

A quantidade de tipos aceitos não define a utilidade do genérico; a relação entre as posições da assinatura define.
Exemplo
# Resultado sempre é texto: não há relação a preservar.
def rotulo(valor: object) -> str:
return str(valor)
# As alternativas são aceitas, mas o retorno é sempre int.
def tamanho(codigo: str | bytes) -> int:
return len(codigo)
# O próprio valor volta: o tipo de entrada determina o retorno.
def identidade[T](valor: T) -> T:
return valorrotulo e tamanho não ganham precisão ao declarar [T]. Já identidade precisa de T nas duas posições para manter a informação.
Relacione o comportamento da função à categoria de assinatura mais adequada.
Toque em um item e depois no par correspondente.
Compare estas versões:
def registrar[T](valor: T) -> None:
print(valor)
def registrar(valor: object) -> None:
print(valor)Como o retorno é None e não há outra posição ligada a T, o primeiro contrato não preserva uma relação útil. A segunda versão comunica diretamente que a função só recebe um valor para exibi-lo.
Da mesma forma, use uma união quando ela descreve alternativas reais sem correlação: def codigo(valor: int | str) -> str sempre produz str, seja qual for a alternativa recebida.
Uma função normalizar aceita str ou bytes e sempre devolve str. Qual assinatura você escolheria? Explique por que usar def normalizar[T](valor: T) -> str não melhora o contrato.
Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8
Integre seleção, transformação e preservação de subtipos em um único arquivo e confirme a inferência com mypy.
Nesta aplicação, cada função responde a uma pergunta diferente:
list[T], obter um T;list[T] e Callable[[T], R], obter list[R];T limitado por uma classe base, usar uma operação da base e devolver o mesmo T.Ao compor as chamadas, acompanhe o tipo estático em cada etapa. O objetivo não é aceitar “qualquer coisa”, e sim manter a informação que já existe no ponto de chamada.
O diagrama mostra como cada assinatura transporta a informação de tipo até o resultado.

Cada seta preserva ou transforma tipos de acordo com a relação declarada.
Crie um arquivo chamado relacoes.py e copie o código abaixo. As implementações já estão completas; substitua apenas as três assinaturas amplas pelas assinaturas genéricas adequadas.
Use parâmetros de tipo declarados diretamente nas funções, na sintaxe do Python 3.12. Não introduza Any, casts nem supressões de diagnóstico.
from collections.abc import Callable
class Documento:
def __init__(self, titulo: str) -> None:
self.titulo = titulo
def descricao(self) -> str:
return self.titulo.upper()
class Relatorio(Documento):
def exportar_pdf(self) -> bytes:
return b"PDF"
# Troque list[object] e object por uma relação list[T] -> T.
def primeiro(itens: list[object]) -> object:
return itens[0]
# Declare tipos independentes para a entrada e a saída.
def transformar(
itens: list[object], funcao: Callable[[object], object]
) -> list[object]:
return [funcao(item) for item in itens]
# Limite o tipo pela classe Documento e preserve o subtipo no retorno.
def registrar(documento: Documento) -> Documento:
print(documento.descricao())
return documento
codigos = ["10", "25"]
numero_inicial = primeiro(codigos)
valores = transformar(codigos, int)
relatorio = Relatorio("Fechamento mensal")
relatorio_registrado = registrar(relatorio)
reveal_type(numero_inicial)
reveal_type(valores)
reveal_type(relatorio_registrado)
Dica
Para cada função, localize as posições que precisam ficar relacionadas:
descricao()? Use um limite superior que garanta essa operação, mas retorne o próprio parâmetro de tipo limitado.Compare somente depois de tentar. Execute python -m mypy --strict relacoes.py e leia os três resultados de reveal_type.
from collections.abc import Callable
class Documento:
def __init__(self, titulo: str) -> None:
self.titulo = titulo
def descricao(self) -> str:
return self.titulo.upper()
class Relatorio(Documento):
def exportar_pdf(self) -> bytes:
return b"PDF"
def primeiro[T](itens: list[T]) -> T:
return itens[0]
def transformar[T, R](
itens: list[T], funcao: Callable[[T], R]
) -> list[R]:
return [funcao(item) for item in itens]
def registrar[T: Documento](documento: T) -> T:
print(documento.descricao())
return documento
codigos = ["10", "25"]
numero_inicial = primeiro(codigos)
valores = transformar(codigos, int)
relatorio = Relatorio("Fechamento mensal")
relatorio_registrado = registrar(relatorio)
reveal_type(numero_inicial) # builtins.str
reveal_type(valores) # builtins.list[builtins.int]
reveal_type(relatorio_registrado) # relacoes.Relatorio
Atenção
Esta implementação viola o contrato, mesmo que Documento satisfaça o limite:
def registrar[T: Documento](documento: T) -> T:
print(documento.descricao())
return Documento("Registro novo")T pode ser Relatorio; portanto, um Documento novo não é necessariamente um T. Retornar documento respeita todos os subtipos admitidos. Se o resultado realmente devesse ser uma nova instância da base, o retorno teria de ser Documento, e a preservação do subtipo deixaria de fazer parte do contrato.
Se o verificador apontar um erro, revise primeiro a relação prometida e todos os caminhos de retorno. Evite “resolver” o diagnóstico com Any ou uma supressão: isso interrompe justamente a evidência estática que você quer acompanhar.
Depois da verificação, remova os reveal_type, pois eles servem apenas à inspeção do mypy.
Resumo
T repetido quando a saída deve manter o tipo relacionado à entrada, como em list[T] -> T.T e R quando uma transformação recebe um tipo e produz outro: list[T], Callable[[T], R] e list[R].T: ClasseBase quando o corpo precisa de operações garantidas pela base, mas o retorno ainda deve preservar o subtipo recebido.Any para ocultar incompatibilidades.Após executar o mypy, relate: quais foram os três tipos revelados? Explique também por que retornar um novo Documento em registrar[T: Documento] não cumpre necessariamente o retorno T.
Escreva pelo menos 120 caracteres (0/120).
Parabéns! Você concluiu: Preservar relações de tipos em funções genéricas
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