Python permite reatribuir nomes, sobrescrever métodos e herdar de quase qualquer classe. Essa flexibilidade é útil, mas algumas partes de uma API deveriam permanecer estáveis: constantes não devem receber outro valor, um atributo de instância deve ser definido apenas uma vez, certos métodos não devem ser sobrescritos e algumas classes não foram projetadas para herança. O módulo typing oferece Final e o decorador @final para expressar essas intenções aos analisadores estáticos.
Neste guia, você aprenderá a usar Final em módulos, classes e instâncias, distinguir Final de imutabilidade real, trabalhar com constantes, dataclasses e configurações, usar @final em métodos e classes e evitar contratos que não correspondem ao comportamento de runtime.
Final em uma constante de módulo
from typing import Final
MAX_TENTATIVAS: Final[int] = 3
URL_API: Final = "https://api.exemplo.com"A primeira forma informa explicitamente o tipo e a intenção final. Na segunda, o analisador infere str a partir do valor. Uma reatribuição posterior deve ser reportada:
MAX_TENTATIVAS = 5 # erro de tipagemPython ainda executa essa atribuição em runtime. Final é uma restrição estática, não uma proteção automática do interpretador.
Final não torna o objeto imutável
ROTAS: Final[list[str]] = ["/inicio"]
ROTAS.append("/ajuda") # permitido
ROTAS = [] # deve ser rejeitado pelo checkerFinal impede a reatribuição do nome, mas não congela o objeto apontado. A lista continua mutável. Para uma coleção realmente imutável, escolha uma representação adequada:
ROTAS_FIXAS: Final[tuple[str, ...]] = ("/inicio", "/ajuda")Mesmo aqui, Final protege o nome estaticamente, enquanto a tupla fornece imutabilidade de estrutura em runtime.
Final e convenção de nomes
Escrever constantes em maiúsculas é apenas uma convenção. Final acrescenta uma regra verificável pelo analisador.
TIMEOUT_PADRAO: Final[float] = 5.0As duas práticas podem ser usadas juntas: maiúsculas ajudam leitores, e Final ajuda ferramentas.
Atributos de classe
class Protocolo:
VERSAO: Final[str] = "1.0"
Subclasses não devem redefinir um atributo Final:
class ProtocoloNovo(Protocolo):
VERSAO = "2.0" # erro estático esperadoSe versões diferentes são parte legítima do design, não use Final nesse atributo. Final deve refletir uma invariável real da hierarquia.
Atributos de instância definidos uma vez
class Sessao:
id: Final[str]
def __init__(self, id_: str) -> None:
self.id = id_
O atributo pode ser inicializado no construtor, mas uma nova atribuição deve ser rejeitada:
sessao = Sessao("abc")
sessao.id = "xyz" # erro de tipagemNovamente, isso não impede a atribuição em runtime. Para reforçar a regra, use propriedades sem setter, dataclasses congeladas ou uma implementação personalizada de __setattr__.
Onde inicializar um atributo Final
Um atributo Final deve ser inicializado em uma posição clara e única, normalmente na declaração da classe ou dentro de __init__. Atribuições condicionais complexas podem dificultar a análise.
class Requisicao:
token: Final[str]
def __init__(self, token: str | None) -> None:
if token is None:
self.token = gerar_token()
else:
self.token = tokenVerificadores modernos podem aceitar caminhos mutuamente exclusivos, desde que todos inicializem o atributo exatamente uma vez. Para máxima clareza, calcule primeiro e atribua depois:
valor = gerar_token() if token is None else token
self.token = valorFinal em dataclasses
from dataclasses import dataclass
from typing import Final
@dataclass
class Evento:
id: Final[str]
payload: dict[str, object]
O comportamento e o suporte podem variar entre analisadores, especialmente quando a dataclass gera __init__. Para imutabilidade de runtime, use @dataclass(frozen=True):
@dataclass(frozen=True)
class EventoImutavel:
id: str
payload: dict[str, object]Mesmo uma dataclass congelada não torna objetos internos imutáveis. Um dicionário dentro dela ainda pode ser alterado. Use estruturas imutáveis se essa for a exigência.
Final com ClassVar
ClassVar indica que um atributo pertence à classe, não às instâncias. Final indica que não deve ser redefinido. Dependendo da versão e do analisador, combinar os dois pode ter limitações sintáticas ou semânticas. Muitas vezes, uma anotação Final diretamente no corpo da classe já comunica a intenção necessária.
Quando o atributo é configurável por subclasses, use ClassVar sem Final. Quando é fixo em toda a hierarquia, use Final e teste com o checker adotado.
Final e Literal
Final protege um nome; Literal descreve um conjunto de valores exatos.
from typing import Final, Literal
MODO_PADRAO: Final[Literal["seguro"]] = "seguro"
Essa combinação raramente é necessária para constantes simples, porque o valor já é evidente. Literal é mais útil em parâmetros e retornos, enquanto Final é útil em definições que não devem mudar. Consulte o guia de Literal no Python.
Final e NewType
from typing import NewType
UsuarioId = NewType("UsuarioId", int)
USUARIO_SISTEMA: Final[UsuarioId] = UsuarioId(1)NewType mantém a distinção semântica do valor e Final impede que o nome seja reatribuído estaticamente.
O decorador @final em métodos
from typing import final
class Autenticador:
@final
def validar_assinatura(self, token: str) -> bool:
return verificar(token)
Uma subclasse não deve sobrescrever o método:
class AutenticadorCustomizado(Autenticador):
def validar_assinatura(self, token: str) -> bool:
return True # erro estático esperadoO decorador comunica que o algoritmo é parte fixa do contrato. Use-o com parcimônia: impedir extensão reduz a flexibilidade da API.
@final em classes
@final
class TokenInterno:
def __init__(self, valor: str) -> None:
self.valor = valor
Analisadores devem rejeitar herança:
class TokenEspecial(TokenInterno): # erro
passIsso é útil quando a classe depende de invariantes que subclasses poderiam quebrar, quando usa otimizações internas ou quando composição é a extensão recomendada.
@final não bloqueia herança em runtime
O decorador do módulo typing não impede automaticamente que o interpretador crie uma subclasse. Em versões modernas, ele pode marcar um atributo de reflexão como __final__, mas a aplicação continua dependendo de ferramentas ou de verificações próprias.
Para bloquear herança em runtime, seria necessário implementar lógica como __init_subclass__:
class ClasseFechada:
def __init_subclass__(cls) -> None:
raise TypeError("herança não permitida")Use essa abordagem apenas quando a proteção de runtime for realmente necessária, pois ela altera o comportamento da linguagem.
Método template com etapas finais
Uma classe pode permitir que subclasses personalizem pontos específicos, mantendo outras etapas fechadas.
class Importador:
def executar(self, caminho: str) -> None:
dados = self.ler(caminho)
dados = self.transformar(dados)
self.salvar(dados)
def ler(self, caminho: str) -> bytes:
raise NotImplementedError
def transformar(self, dados: bytes) -> bytes:
return dados
@final
def salvar(self, dados: bytes) -> None:
gravar_com_auditoria(dados)O método salvar permanece fixo porque contém uma exigência de auditoria, enquanto leitura e transformação são pontos de extensão.
Final em Protocol
Protocol descreve comportamento estrutural. Final e @final são mais ligados a implementação e herança nominal. Aplicá-los em protocolos geralmente não oferece o mesmo valor, pois classes compatíveis não precisam herdar do Protocol. Para contratos estruturais, documente a semântica e use métodos ou propriedades apropriadas.
Final e propriedades
Uma propriedade somente leitura oferece proteção prática em runtime:
class Conta:
def __init__(self, numero: str) -> None:
self._numero = numero
@property
def numero(self) -> str:
return self._numeroO consumidor não pode atribuir conta.numero sem um setter. Final pode ser usado no armazenamento interno para reforçar a intenção estática:
class Conta:
_numero: Final[str]
Configurações carregadas em runtime
Uma configuração pode ser final depois da inicialização, mesmo que seu valor seja lido de ambiente ou arquivo:
class Configuracao:
ambiente: Final[str]
timeout: Final[float]
def __init__(self) -> None:
self.ambiente = ler_ambiente()
self.timeout = ler_timeout()Final não exige que o valor seja conhecido em tempo de compilação; exige apenas que o nome não seja reatribuído depois de inicializado.
Constantes e importações
Importar um nome Final para outro módulo não impede manipulações dinâmicas. O benefício aparece quando todos os módulos são analisados e respeitam as anotações. Evite depender de Final como mecanismo de segurança contra código externo.
Final e monkey patching
Python permite alterar atributos de módulos e classes dinamicamente. Final e @final sinalizam que isso viola o contrato, mas não desativam monkey patching. Testes que substituem dependências devem preferir pontos de injeção explícitos em vez de alterar membros finais.
Quando não usar Final
- Quando subclasses devem configurar o atributo.
- Quando reatribuição faz parte do ciclo de vida.
- Quando a API pública ainda está em evolução e precisa de extensibilidade.
- Quando você quer apenas imutabilidade do objeto; Final protege o nome.
- Quando a equipe não executa nenhum analisador estático.
Erros comuns
- Confundir Final com const de runtime: o interpretador ainda permite reatribuição.
- Anotar uma lista como Final e esperar congelamento: o conteúdo continua mutável.
- Aplicar @final a todo método: a hierarquia fica difícil de estender.
- Marcar como Final algo configurável: o contrato contradiz o design.
- Usar Final sem inicialização clara: verificadores podem rejeitar ou inferir incorretamente.
- Confiar em Final para segurança: dados e permissões precisam de validação real.
Exemplo completo: cliente de API estável
from typing import Final, final
URL_PADRAO: Final[str] = "https://api.exemplo.com"
class ClienteApi:
url: Final[str]
_cabecalho_versao: Final[str] = "X-Api-Version"
def __init__(self, url: str = URL_PADRAO) -> None:
self.url = url.rstrip("/")
def obter(self, caminho: str) -> bytes:
resposta = self._enviar("GET", caminho)
return resposta
@final
def _enviar(self, metodo: str, caminho: str) -> bytes:
headers = {self._cabecalho_versao: "1"}
return transporte_http(
metodo,
f"{self.url}/{caminho.lstrip('/')}",
headers=headers,
)
class ClienteComCache(ClienteApi):
def obter(self, caminho: str) -> bytes:
if dado := cache_buscar(caminho):
return dado
dado = super().obter(caminho)
cache_salvar(caminho, dado)
return dadoA URL de cada instância é definida uma vez, o nome do cabeçalho é fixo na hierarquia e o envio centralizado não pode ser sobrescrito. A operação de alto nível obter() permanece extensível para cache.
Testando o contrato
Inclua o checker no CI e crie arquivos de teste de tipagem para reatribuições e sobrescritas inválidas. Testes de runtime devem verificar imutabilidade real somente quando ela é implementada por tuplas, propriedades, dataclasses congeladas ou outras estruturas concretas.
Conclusão
typing.Final protege nomes e atributos contra reatribuição no contrato estático, enquanto @final impede sobrescrita e herança para analisadores. Essas ferramentas ajudam a documentar invariantes e reduzir extensões acidentais.
A documentação oficial de Final e do decorador final no Python detalha a semântica. Combine-as com estruturas imutáveis ou controles de runtime quando a regra precisar ser realmente aplicada pelo programa.







