
Passo 1 de 8
Representar tipos alternativos com |
Use o operador | para declarar contratos que aceitam tipos alternativos e reconheça quando uma operação ainda não é segura.
Trilha de aprendizado · Nível 12 · Tutorial 3
Representar valores que admitem tipos alternativos e organizar condições para que cada operação seja aplicada somente aos tipos compatíveis.
Representar tipos alternativos com |
Use o operador | para declarar contratos que aceitam tipos alternativos e reconheça quando uma operação ainda não é segura. 2 min
Separar aceitar None de permitir omissão
Diferencie o valor None de um argumento ausente e escreva assinaturas coerentes com cada contrato. 2 min
Distinguir união de elementos e união de coleções
Use a posição de | e dos colchetes para expressar se a alternativa vale para cada elemento ou para a lista inteira. 2 min
Tratar resultados opcionais com verificações de None
Proteja o resultado de dict.get antes de usar operações que exigem um valor presente. 3 min
Separar alternativas com isinstance
Use isinstance para identificar qual alternativa de uma união está presente e aplicar operações compatíveis em cada ramo. 2 min
Estreitar tipos com saídas antecipadas
Use cláusulas de guarda para encerrar casos que não devem chegar ao processamento principal e deixar explícito qual tipo permanece. 2 min
Reavaliar tipos quando o fluxo muda
Acompanhe os tipos possíveis depois que caminhos se unem, terminam ou recebem uma nova atribuição. 2 min
Aplicar o tratamento seguro em uma consulta tipada
Integre argumentos opcionais, dict.get e estreitamento de tipos para corrigir uma função e verificá-la com Python e mypy. 4 min

Passo 1 de 8
Use o operador | para declarar contratos que aceitam tipos alternativos e reconheça quando uma operação ainda não é segura.
Use | quando um valor pode seguir um entre tipos alternativos. Por exemplo, int | str informa que o contrato aceita um número inteiro ou um texto.
A união descreve possibilidades; ela não converte valores. Se uma variável recebe "42", ela continua sendo str, mesmo que o contrato também admita int.
Leia a anotação como duas possibilidades para o mesmo valor.

int | str permite uma alternativa ou outra, não transforma uma na outra.
A mesma anotação pode aparecer em diferentes pontos do contrato.
codigo: int | str = 404
def exibir_codigo(valor: int | str) -> str:
return f"Código: {valor}"
print(exibir_codigo(codigo))
print(exibir_codigo("indisponível"))Na variável, a união limita os valores que podem ser atribuídos. No parâmetro, limita os argumentos aceitos. No retorno, informa que a chamada pode produzir qualquer uma das alternativas declaradas.
Como nas outras anotações, isso é um contrato para análise estática: Python não passa a validar automaticamente os valores em tempo de execução. O mypy pode apontar usos incompatíveis ao analisar o código.
Enquanto valor pode ser int ou str, uma operação só é segura se funcionar para ambos os tipos. str(valor) é aceitável: tanto inteiros quanto textos podem ser representados como texto.
Já valor + 1 não é justificado pelo contrato: funciona para int, mas não para str. O mypy sinaliza esse tipo de risco; a anotação não escolhe qual alternativa chegou na execução.
O comentário indica o ponto que o mypy deve rejeitar.
def aumentar(valor: int | str) -> int | str:
# Erro: str não pode ser somada a int.
return valor + 1
def rotulo(valor: int | str) -> str:
# Seguro para as duas alternativas.
return "Valor: " + str(valor)Uma função recebe um identificador que pode ser o inteiro 15 ou o texto "A15". Qual anotação representa esse parâmetro?
Com valor: int | str, qual expressão ainda é insegura sem uma justificativa adicional no fluxo do código?

Passo 2 de 8
Diferencie o valor None de um argumento ausente e escreva assinaturas coerentes com cada contrato.
Em uma anotação, str | None significa que o valor pode ser um texto ou None. Isso não diz se quem chama a função pode deixar o argumento de fora.
A possibilidade de omitir depende de existir um valor padrão na assinatura. Portanto, são duas decisões independentes:
str ou str | None.= algum_valor.Compare o que cada parte da assinatura controla.

A anotação define valores válidos; o padrão permite omissão do argumento.
Cada função ilustra uma combinação diferente. O corpo foi omitido porque o foco é o contrato do parâmetro.
# 1. Obrigatório; aceita somente str
def rotulo(nome: str) -> str:
return nome
# rotulo() # erro: argumento ausente
# rotulo(None) # erro de tipo
rotulo("Ana")
# 2. Obrigatório; aceita str ou None
def apelido(nome: str | None) -> str:
return "visitante" if nome is None else nome
# apelido() # erro: argumento ausente
apelido(None)
apelido("Ana")
# 3. Pode ser omitido; aceita somente str
def saudacao(nome: str = "visitante") -> str:
return f"Olá, {nome}!"
saudacao()
# saudacao(None) # erro de tipo
saudacao("Ana")
# 4. Pode ser omitido; aceita str ou None
def mensagem(nome: str | None = None) -> str:
return "Sem nome" if nome is None else f"Olá, {nome}!"
mensagem()
mensagem(None)
mensagem("Ana")Dica
O valor padrão também é um valor possível para o parâmetro. Assim, def mensagem(nome: str = None) é incoerente: None não pertence a str. Use str | None = None ou escolha um padrão que seja str.
A forma T | None pode aparecer onde um valor talvez esteja ausente: em uma variável local, em um parâmetro ou no retorno de uma função. Em todos os casos, ela descreve as alternativas possíveis do valor.
Observe que somente consultar_id pode retornar None; já nome_digitado é uma variável que pode guardar esse valor.
nome_digitado: str | None = None
def consultar_id(usuario: str) -> int | None:
if usuario == "ana":
return 42
return None
id_encontrado: int | None = consultar_id("bia")Relacione cada assinatura à descrição correta.
Toque em um item e depois no par correspondente.
Complete somente a anotação: def limite(maximo: ___ = None) -> int:

Passo 3 de 8
Use a posição de | e dos colchetes para expressar se a alternativa vale para cada elemento ou para a lista inteira.
Compare as duas anotações:
list[int | str]: a união está dentro dos colchetes. Cada elemento da mesma lista pode ser int ou str.list[int] | list[str]: a união está fora dos colchetes. O valor inteiro é uma lista de inteiros ou uma lista de textos.Portanto, mover | muda o contrato.
A posição de | indica se a escolha ocorre elemento a elemento ou para a coleção como um todo.

Dentro dos colchetes: alternativas para cada elemento. Fora: alternativas para a lista inteira.
Exemplo
valores: list[int | str] = [10, "dez", 20, "vinte"]Aqui, cada posição aceita uma das duas alternativas. Por isso, inteiros e textos podem aparecer juntos na mesma lista.
Também seriam compatíveis: [], [1, 2] e ["a", "b"].
Dica
Em list[int | str], primeiro leia o tipo de um elemento: int | str. Depois leia list[...]: uma lista desses elementos.
Exemplo
numeros: list[int] | list[str] = [10, 20, 30]
textos: list[int] | list[str] = ["dez", "vinte"]Cada valor é uma lista uniforme: ou todos os elementos são int, ou todos são str.
Já [10, "vinte"] não corresponde a esse contrato, pois mistura os tipos na mesma lista.
Para escolher a anotação, observe o formato permitido:
[1, "dois"]? Use list[int | str].[1, 2] ou ["um", "dois"], mas não uma mistura? Use list[int] | list[str].Essa comparação descreve contratos por valores literais; não depende de regras de compatibilidade entre variáveis de listas.
Qual anotação representa uma lista etiquetas que pode conter valores como [101, "novo", 205]?

Passo 4 de 8
Proteja o resultado de dict.get antes de usar operações que exigem um valor presente.
Mesmo que um dicionário armazene apenas inteiros, uma busca com get sem valor padrão pode não encontrar a chave. Por isso, o resultado admite None.
Em dict[str, int], a expressão pontos.get("ana") tem tipo int | None: o int vem de um valor encontrado; o None, de uma chave ausente.
A anotação dos valores do dicionário não elimina a possibilidade de a chave consultada estar ausente.

get pode devolver um valor armazenado ou None quando não encontra a chave.
A operação abaixo seria insegura sem uma verificação.
pontos: dict[str, int] = {"ana": 12, "bia": 7}
pontuacao: int | None = pontos.get("cai")
print(pontuacao) # None
# pontuacao + 1 # inseguro: pontuacao pode ser NoneGuarde a consulta em uma variável local e teste-a com is None. No ramo verdadeiro, ela é None. No else, essa possibilidade foi excluída: ali, pontuacao é int e pode participar da soma.
Você também pode escrever a condição positiva como is not None; nesse caso, o ramo verdadeiro recebe o valor presente.
A soma está no ramo em que None não é mais uma possibilidade.
pontos: dict[str, int] = {"ana": 12, "bia": 7}
pontuacao: int | None = pontos.get("ana")
if pontuacao is None:
print("Pessoa não encontrada")
else:
proxima_pontuacao = pontuacao + 1
print(f"Próxima pontuação: {proxima_pontuacao}")Atenção
if pontuacao: não responde à pergunta “a chave foi encontrada?”. Um valor presente pode ser 0, que é falso em um contexto booleano. O mesmo ocorre com "" em dicionários de textos.
Para verificar a ausência produzida por get, use is None ou is not None.
Nos dois exemplos, a chave existe; apenas seus valores são falsos em uma condição booleana.
quantidades: dict[str, int] = {"maçã": 0}
quantidade: int | None = quantidades.get("maçã")
if quantidade is None:
print("Produto ausente")
else:
print(f"Quantidade registrada: {quantidade}") # 0
nomes: dict[str, str] = {"apelido": ""}
apelido: str | None = nomes.get("apelido")
if apelido is None:
print("Chave ausente")
else:
print("A chave existe, mesmo com texto vazio")Complete a condição para mostrar a mensagem apenas quando a consulta não encontrar a chave:
estoque: dict[str, int] = {"caneta": 0}
quantidade: int | None = estoque.get("caderno")
if quantidade ___:
print("Produto não encontrado")
else:
print(quantidade + 1)Com o código da atividade, qual mensagem seria exibida se a consulta fosse estoque.get("caneta")?

Passo 5 de 8
Use isinstance para identificar qual alternativa de uma união está presente e aplicar operações compatíveis em cada ramo.
Uma anotação como int | str informa que a variável pode conter uma das duas alternativas. Antes de usar uma operação exclusiva de um tipo, use isinstance para descobrir qual alternativa está presente naquele ramo.
Em if isinstance(valor, int):, o ramo verdadeiro trabalha com int. Como a união simples tem apenas int e str, no else resta str.
A condição divide as alternativas ainda possíveis em ramos seguros.

Cada ramo recebe um contrato mais específico do que o contrato inicial.
A multiplicação e upper() não são justificadas para os dois tipos ao mesmo tempo. Elas ficam nos ramos que comprovam seu tipo.
def apresentar(valor: int | str) -> str:
if isinstance(valor, int):
dobro = valor * 2
return f"Número dobrado: {dobro}"
else:
destaque = valor.upper()
return f"Texto em destaque: {destaque}"
print(apresentar(7)) # Número dobrado: 14
print(apresentar("azul")) # Texto em destaque: AZULDica
O else não significa “qualquer outro valor” neste contrato. Como valor começou como int | str e o if confirmou int, ali resta somente str.
Quando o valor é int | str | None, uma verificação de ausência vem antes: após confirmar que não é None, restam int | str. Então isinstance separa essas duas alternativas.
Cada operação aparece somente depois da evidência que a justifica.
O segundo if é alcançado apenas quando dado não é None.
def descrever(dado: int | str | None) -> str:
if dado is None:
return "Sem dado"
if isinstance(dado, int):
return f"Quadrado: {dado ** 2}"
else:
return f"Caracteres: {len(dado)}"Considere a função descrever acima. Relacione cada situação ao contrato de dado naquele ponto.
Toque em um item e depois no par correspondente.

Passo 6 de 8
Use cláusulas de guarda para encerrar casos que não devem chegar ao processamento principal e deixar explícito qual tipo permanece.
Uma cláusula de guarda trata logo no início um caso que não seguirá para o processamento principal. Se o parâmetro é str | None, teste None e termine esse ramo com return. Depois da guarda, o único tipo possível para nome é str.
O ramo que recebe None termina; apenas o ramo com texto alcança upper().

Um caminho encerrado não alcança as operações posteriores.
O retorno representa uma resposta válida para a ausência de nome.
def saudacao(nome: str | None) -> str:
if nome is None:
return "Olá, visitante!"
return f"Olá, {nome.upper()}!"
print(saudacao(None)) # Olá, visitante!
print(saudacao("Lia")) # Olá, LIA!Escolha a saída conforme o contrato da função. Use return quando a alternativa é prevista e há uma resposta útil para ela. Use raise quando aquela alternativa viola uma exigência da função. Nos dois casos, o ramo termina: o processamento seguinte só é alcançado pelos tipos restantes.
Aqui, a função exige uma idade numérica. None não tem resposta alternativa: é uma entrada inválida.
def ano_de_nascimento(idade: int | None, ano_atual: int) -> int:
if idade is None:
raise ValueError("A idade é obrigatória.")
return ano_atual - idade
print(ano_de_nascimento(30, 2025)) # 1995Dica
Um if idade is None: sem return ou raise não elimina None do trecho posterior: esse caminho ainda pode continuar até ele.
Coloque os blocos na ordem que forma uma função segura para apresentar um código opcional.

Passo 7 de 8
Acompanhe os tipos possíveis depois que caminhos se unem, terminam ou recebem uma nova atribuição.
Uma anotação como int | str continua sendo o contrato da variável. O estreitamento obtido por uma condição vale apenas onde o fluxo fornece essa evidência.
Quando dois caminhos que continuam se reencontram, o mypy combina os tipos que podem chegar àquele ponto. Por isso, uma união pode voltar a ser relevante após um if.

Depois da junção, chegam os valores dos dois ramos que continuam.
Use reveal_type somente ao verificar o arquivo com mypy; remova-o antes da execução normal.
def mostrar(valor: int | str) -> None:
if isinstance(valor, int):
reveal_type(valor) # mypy: builtins.int
print(valor + 1)
else:
reveal_type(valor) # mypy: builtins.str
print(valor.upper())
reveal_type(valor) # mypy: builtins.int | builtins.str
# valor.upper() seria inseguro aqui
Um caminho que termina com return ou raise não alcança o código seguinte. Assim, seu tipo não participa da junção.
No exemplo, após retornar no caso de None, o restante da função recebe somente int. A anotação do parâmetro não mudou; o fluxo eliminou uma alternativa naquele ponto.
def dobrar(valor: int | None) -> int:
if valor is None:
return 0
reveal_type(valor) # mypy: builtins.int
return valor * 2
Em dobrar, qual tipo o mypy conhece para valor imediatamente antes de return valor * 2?
O tipo conhecido também é atualizado após uma nova atribuição compatível com a anotação declarada. Se uma variável declarada como str | None recebe None novamente, operações de texto deixam de estar justificadas até outra verificação.
Não basta lembrar que a variável já foi str em um trecho anterior: analise o ponto exato da operação.
def preparar(nome: str | None, limpar: bool) -> str:
if nome is None:
nome = "visitante"
reveal_type(nome) # mypy: builtins.str
if limpar:
nome = None
reveal_type(nome) # mypy: builtins.str | None
if nome is None:
return "sem nome"
return nome.title()
Dica
Antes de chamar um método ou aplicar um operador específico, pergunte: quais caminhos e quais atribuições podem chegar aqui? A operação precisa ser válida para todos os tipos ainda possíveis.
Analise o código e explique por que uma nova verificação é necessária na linha marcada:
<code>def exibir(codigo: str | None, redefinir: bool) -> str:
if codigo is None:
codigo = "pendente"
if redefinir:
codigo = None
# linha marcada
return codigo.upper()</code>
Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8
Integre argumentos opcionais, dict.get e estreitamento de tipos para corrigir uma função e verificá-la com Python e mypy.
Nesta função, chave: str | None = None aceita uma chave de texto ou None; o = None permite que o argumento seja omitido. Ao chamar apresentar(), a função recebe o padrão None. Ao chamar apresentar(None), o chamador fornece None explicitamente. Dentro desta função, ambos seguem o mesmo caso de “nenhuma chave informada”.
Depois da guarda para chave, catalogo.get(chave) tem tipo int | str | None: a chave pode não existir. Só depois de excluir None é seguro separar int de str com isinstance.
Cada ramo elimina possibilidades antes de executar uma operação específica.

As saídas antecipadas removem None dos caminhos que continuam. No trecho final, cada operação é justificada pelo tipo restante.
No seu editor, crie um arquivo chamado consulta_tipado.py com o código abaixo. Substitua os # TODO sem usar Any, cast ou supressões do mypy.
Use saídas antecipadas para: (1) tratar chave is None; (2) tratar uma chave ausente; e então use isinstance(valor, int) antes da soma. No ramo restante, aplique upper() ao texto.
Corrija somente os trechos marcados com TODO.
catalogo: dict[str, int | str] = {
"itens": 4,
"status": "ativo",
"zero": 0,
"vazio": "",
}
def apresentar(chave: str | None = None) -> str:
# TODO: encerre o caso em que chave é None.
valor = catalogo.get(chave)
# TODO: encerre o caso em que a chave não foi encontrada.
# TODO: no caso int, retorne o valor acrescido de 1.
# TODO: no outro caso, retorne o texto em maiúsculas.
return f"{chave}: {valor + 1}"
print(apresentar("itens"))
print(apresentar("status"))
print(apresentar("zero"))
print(apresentar("vazio"))
print(apresentar("inexistente"))
print(apresentar())
print(apresentar(None))Compare com sua versão depois de tentar. A mesma guarda que torna a chamada de get válida também elimina None no restante da função.
catalogo: dict[str, int | str] = {
"itens": 4,
"status": "ativo",
"zero": 0,
"vazio": "",
}
def apresentar(chave: str | None = None) -> str:
if chave is None:
return "Nenhuma chave informada"
valor = catalogo.get(chave)
if valor is None:
return f"{chave}: ausente"
if isinstance(valor, int):
return f"{chave}: {valor + 1} unidades"
return f"{chave}: {valor.upper()}"
print(apresentar("itens"))
print(apresentar("status"))
print(apresentar("zero"))
print(apresentar("vazio"))
print(apresentar("inexistente"))
print(apresentar())
print(apresentar(None))Exemplo
No terminal, na pasta do arquivo, execute:
python consulta_tipado.py
python -m mypy --strict consulta_tipado.pyA execução deve produzir, nesta ordem:
itens: 5 unidades
status: ATIVO
zero: 1 unidades
vazio:
inexistente: ausente
Nenhuma chave informada
Nenhuma chave informadaO mypy deve terminar sem erros. Note que 0 e "" chegam aos ramos de inteiro e texto: eles são valores presentes, portanto não devem ser tratados com if not valor.
Dica
Após if chave is None: return, o único tipo possível para chave é str. Após if valor is None: return, valor só pode ser int | str. Depois de isinstance(valor, int), o ramo final recebe str. Cada return encerra um caminho, então ele não contribui para os tipos possíveis adiante.
Depois de executar sua versão, relate: quais foram os resultados para inteiro, texto, 0, texto vazio, chave ausente, argumento omitido e None explícito? Justifique por que usou is None e isinstance, em vez de um teste de valor booleano.
Escreva pelo menos 180 caracteres (0/180).
Resumo
Use o contrato e o fluxo juntos para justificar cada operação.
T | None quando o valor pode ser None; um valor padrão compatível decide se o argumento pode ser omitido.dict.get sem padrão pode produzir None, mesmo quando os valores do dicionário são int | str.is None, não com a verdade do valor: 0 e "" podem ser dados válidos.isinstance para separar alternativas e aplique soma ou métodos de texto apenas no ramo compatível.return ou raise antecipado remove daquele restante do fluxo a alternativa que já foi tratada.Parabéns! Você concluiu: Tratar valores opcionais e uniões de tipos
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