Desvendando Never e NoReturn: Os Tipos Especiais do Python
Entenda a diferença entre top types e bottom types e quando usar Never e NoReturn para um código mais expressivo e seguro com tipagem estática.
Recentemente, mergulhei nas PEPs e discussões da comunidade Python para entender a fundo dois tipos que, embora pareçam complexos, simplificam a forma como lidamos com funções que nunca retornam e código inalcançável: Never e NoReturn. Eles são a chave para um sistema de tipagem mais robusto e uma comunicação mais clara das intenções do nosso código.
Em vídeo
Se preferir, você pode assistir ao vídeo para entender melhor.
Top Types vs. Bottom Types: Onde Tudo Começa
Para entender Never e NoReturn, primeiro precisamos dar um passo atrás e falar sobre a hierarquia de tipos no Python.
No topo da pirâmide, temos os Top Types, como object e Any. object é o tipo base para todas as classes em Python, o supertipo de todos os outros. Any é um caso especial: ele é compatível com qualquer tipo e, ao mesmo tempo, qualquer tipo é compatível com ele. O problema é que Any efetivamente desliga o type checker, o que nos faz perder as garantias da tipagem estática. Eu, particularmente, evito usá-lo, exceto em situações extremas.
# object é o supertipo de todos
# Any é compatível com tudo (e desliga o type checker)
top_types: list[object | any] = [1, "string", True, []]Na base da pirâmide, encontramos os Bottom Types. Se um top type é um “super-tipo” de todos, um bottom type é um “sub-tipo” de todos. E é exatamente aqui que Never e NoReturn entram. Como um valor pode ser um subtipo de todos os outros tipos possíveis (como int, str, list, etc.) ao mesmo tempo? A única resposta lógica é que esse valor nunca pode existir. Ele representa um ponto no código que nunca será alcançado.
NoReturn: Funções Que Nunca Retornam
A diferença entre Never e NoReturn é puramente semântica, já que os type checkers os tratam da mesma forma. Eu prefiro usar NoReturn para anotar funções que garantidamente nunca concluem sua execução normal, ou seja, nunca retornam um valor ao chamador.
Isso acontece em dois cenários principais:
A função encerra o programa.
A função sempre lança uma exceção.
Veja este exemplo com sys.exit:
import sys
from typing import NoReturn
def sair_do_programa(mensagem: str) -> NoReturn:
"""Encerra a execução do programa com uma mensagem."""
print(f"Encerrando: {mensagem}")
sys.exit(1)
# O type checker sabe que o código abaixo de sair_do_programa() é inalcançável
print("Iniciando o programa...")
sair_do_programa("Ocorreu um erro fatal.")
print("Este trecho nunca será executado.") # Alerta do type checker: "code is unreachable"Ao tipar a função com NoReturn, o type checker entende que qualquer código após uma chamada a sair_do_programa é inalcançável, nos ajudando a evitar código morto (dead code). Embora anotar com None não gere um erro, é semanticamente incorreto, pois a função não retorna o valor None; ela simplesmente não retorna.
Never: Código Inalcançável e Esgotamento de Condicionais
Eu reservo o Never para situações em que um trecho de código não deveria ser alcançado devido à lógica do programa, como em um loop infinito ou em condicionais exaustivas.
Imagine uma função que processa comandos em um loop. Se o usuário digitar “quit”, o programa sai. O loop em si nunca termina, então a parte do código após o loop é, por definição, inalcançável.
from typing import NoReturn, Never
def quit_program() -> NoReturn:
"""Função auxiliar para sair."""
print("Tchau!")
sys.exit(0)
def loop_de_comandos() -> Never:
"""Processa comandos do usuário em um loop infinito."""
while True:
comando = input("Digite um comando (ou 'quit' para sair): ")
if comando.lower() == 'quit':
quit_program()
else:
print(f"Eco: {comando}")
# Este ponto é estruturalmente inalcançável por causa do `while True`
# e da chamada a `quit_program()`
print("Este código nunca será alcançado.")Outro uso excelente para Never é garantir que uma estrutura condicional (como if/elif/else ou match/case) cobriu todos os casos possíveis. Se você adicionar um else final que resulta em um tipo Never, o type checker irá alertá-lo se um novo caso for adicionado no futuro sem ser tratado.
from typing import Literal, Never
def processar_status(status: Literal['sucesso', 'falha']) -> str:
if status == 'sucesso':
return "Operação bem-sucedida."
elif status == 'falha':
return "A operação falhou."
else:
# Se chegarmos aqui, algo está errado com a lógica.
# O type checker nos avisará se `status` puder ser algo diferente.
# Por exemplo, se adicionarmos 'pendente' ao Literal sem tratar no if/elif.
never_check: Never = status
raise AssertionError(f"Caso não tratado: {never_check}")Conclusão: A Diferença é a Clareza
No fim das contas, a escolha entre NoReturn e Never é sobre semântica e clareza.
Use
NoReturnquando uma função inteira nunca retorna ao seu chamador.Use
Neverpara marcar trechos de código que são inalcançáveis por causa da lógica do programa.
Se ficar na dúvida, usar Never é uma aposta segura, pois ele cobre mais cenários. Adotar esses tipos torna seu código não apenas mais seguro e à prova de erros, mas também mais fácil de entender para quem for lê-lo no futuro.


