Final en Python: protege constantes y herencia

Publicado el: 29/08/2026
Tempo de leitura: 6 minutos
Macro shot capturing detailed patterns of a python in its natural surroundings.

Python permite reasignar nombres, sobrescribir métodos y heredar de casi cualquier clase. Esa flexibilidad es útil, pero algunas partes de una API deben permanecer estables: las constantes no deberían recibir otro valor, ciertos atributos de instancia deberían asignarse una sola vez, algunos métodos no deberían sobrescribirse y determinadas clases no fueron diseñadas para herencia. El módulo typing ofrece Final y el decorador @final para expresar estas intenciones a los analizadores estáticos.

Esta guía cubre constantes de módulo, atributos de clase e instancia, la diferencia entre Final e inmutabilidad real, dataclasses, configuración, métodos y clases finales, controles de runtime y errores de diseño frecuentes.

Constante de módulo

from typing import Final

MAX_INTENTOS: Final[int] = 3
URL_API: Final = "https://api.ejemplo.com"

La primera forma declara tipo e intención final. La segunda permite inferir str. Una reasignación posterior debería marcarse:

MAX_INTENTOS = 5  # error de tipado

Python aún puede ejecutar esa asignación en runtime. Final es un contrato estático, no una restricción automática del intérprete.

Final no congela el objeto

RUTAS: Final[list[str]] = ["/inicio"]
RUTAS.append("/ayuda")  # permitido
RUTAS = []               # debería rechazarse

Final impide cambiar la referencia del nombre, pero no vuelve inmutable la lista. Para una estructura inmutable, elige una representación apropiada:

RUTAS_FIJAS: Final[tuple[str, ...]] = ("/inicio", "/ayuda")

Final protege el nombre estáticamente y la tupla aporta inmutabilidad estructural en runtime.

Convención de nombres y Final

Escribir constantes en mayúsculas es solo una convención. Final añade una regla verificable.

TIMEOUT_PREDETERMINADO: Final[float] = 5.0

Usa ambas prácticas: las mayúsculas ayudan a lectores y Final ayuda a herramientas.

Atributos de clase

class Protocolo:
    VERSION: Final[str] = "1.0"

Una subclase no debería redefinir ese atributo:

class ProtocoloNuevo(Protocolo):
    VERSION = "2.0"  # error estático esperado

Si las versiones distintas forman parte del diseño, no marques el atributo como Final. La anotación debe reflejar una invariante real.

Atributos de instancia asignados una vez

class Sesion:
    id: Final[str]

    def __init__(self, id_: str) -> None:
        self.id = id_

Una asignación posterior debería rechazarse:

sesion = Sesion("abc")
sesion.id = "xyz"  # error de tipado

Esto no bloquea la asignación en runtime. Usa propiedades de solo lectura, dataclasses congeladas o lógica de __setattr__ cuando necesites enforcement real.

Dónde inicializar un atributo Final

Debe inicializarse de forma clara y una sola vez, normalmente en el cuerpo de la clase o en __init__. Las rutas condicionales complejas dificultan el análisis.

class Solicitud:
    token: Final[str]

    def __init__(self, token: str | None) -> None:
        valor = generar_token() if token is None else token
        self.token = valor

Calcular primero y asignar después suele ser el patrón más claro.

Final en dataclasses

from dataclasses import dataclass

@dataclass
class Evento:
    id: Final[str]
    payload: dict[str, object]

El soporte puede variar porque la dataclass genera __init__. Para inmutabilidad de runtime, usa @dataclass(frozen=True):

@dataclass(frozen=True)
class EventoInmutable:
    id: str
    payload: dict[str, object]

Una dataclass congelada tampoco congela objetos internos. El diccionario sigue siendo mutable salvo que uses una estructura inmutable.

Final y ClassVar

ClassVar indica que un atributo pertenece a la clase. Final indica que no debería redefinirse. La combinación puede tener limitaciones según la versión y el analizador. Una anotación Final en el cuerpo de la clase suele ser suficiente.

Usa ClassVar sin Final cuando las subclases deban configurar el valor. Usa Final cuando sea fijo en toda la jerarquía y verifica el comportamiento con el checker del proyecto.

Final y Literal

Final protege un nombre; Literal describe valores exactos.

from typing import Literal

MODO_PREDETERMINADO: Final[Literal["seguro"]] = "seguro"

La combinación rara vez es necesaria para una constante simple. Literal es especialmente útil en parámetros y retornos, y Final en definiciones que no deben cambiar.

Final y NewType

from typing import NewType

UsuarioId = NewType("UsuarioId", int)
USUARIO_SISTEMA: Final[UsuarioId] = UsuarioId(1)

NewType conserva el significado del valor y Final evita la reasignación estática del nombre.

El decorador @final en métodos

from typing import final

class Autenticador:
    @final
    def verificar_firma(self, token: str) -> bool:
        return verificar(token)

Una subclase no debería sobrescribir el método:

class AutenticadorCustom(Autenticador):
    def verificar_firma(self, token: str) -> bool:
        return True  # error esperado

El decorador indica que el algoritmo es parte fija del contrato. Úsalo con moderación porque limita la extensibilidad.

@final en clases

@final
class TokenInterno:
    def __init__(self, valor: str) -> None:
        self.valor = valor

El analizador debería rechazar herencia:

class TokenEspecial(TokenInterno):  # error
    pass

Es apropiado cuando una subclase podría romper invariantes, la implementación depende de optimizaciones internas o la composición es el modelo recomendado.

@final no bloquea herencia en runtime

El decorador de typing normalmente no impide al intérprete crear una subclase. Versiones modernas pueden establecer una marca como __final__, pero el enforcement depende de herramientas o lógica propia.

class ClaseCerrada:
    def __init_subclass__(cls) -> None:
        raise TypeError("herencia no permitida")

Bloquea herencia en runtime solo cuando sea realmente necesario, porque cambia el comportamiento normal de Python.

Método template con pasos finales

class Importador:
    def ejecutar(self, ruta: str) -> None:
        datos = self.leer(ruta)
        datos = self.transformar(datos)
        self.guardar(datos)

    def leer(self, ruta: str) -> bytes:
        raise NotImplementedError

    def transformar(self, datos: bytes) -> bytes:
        return datos

    @final
    def guardar(self, datos: bytes) -> None:
        escribir_con_auditoria(datos)

Lectura y transformación son puntos de extensión, mientras guardar permanece fijo porque la auditoría es obligatoria.

Final en Protocol

Protocol describe comportamiento estructural, mientras Final y @final restringen sobre todo implementaciones nominales y herencia. Una clase compatible no tiene que heredar del Protocol, por lo que los marcadores finales allí suelen aportar poco.

Final y propiedades

Una propiedad sin setter ofrece protección práctica:

class Cuenta:
    def __init__(self, numero: str) -> None:
        self._numero = numero

    @property
    def numero(self) -> str:
        return self._numero

El almacenamiento interno también puede anotarse Final para reforzar la intención estática.

Configuración cargada en runtime

Un valor puede ser final después de inicializarse aunque provenga del entorno o de un archivo.

class Configuracion:
    entorno: Final[str]
    timeout: Final[float]

    def __init__(self) -> None:
        self.entorno = leer_entorno()
        self.timeout = leer_timeout()

Final no exige que el valor se conozca en tiempo de compilación; exige que no se reasigne después.

Importaciones y monkey patching

Importar un nombre Final no impide cambios dinámicos. Python también permite modificar atributos de módulos y clases. Final señala que esas acciones violan el contrato, pero no las desactiva. En pruebas, prefiere puntos explícitos de inyección.

Cuándo no usar Final

  • Las subclases deben configurar el valor.
  • La reasignación forma parte del ciclo de vida.
  • La API está diseñada para extensión.
  • Necesitas inmutabilidad del objeto, no estabilidad del nombre.
  • El proyecto no ejecuta un analizador estático.

Errores comunes

  • Confundir Final con const de runtime: Python aún permite asignar.
  • Anotar una lista y esperar congelación: el contenido sigue mutable.
  • Marcar todos los métodos final: la jerarquía se vuelve difícil de extender.
  • Marcar valores configurables: el contrato contradice el diseño.
  • Usar rutas de inicialización confusas: el analizador puede rechazarlas.
  • Tratar Final como seguridad: permisos y datos necesitan validación real.

Ejemplo completo: cliente de API estable

from typing import Final, final

URL_PREDETERMINADA: Final[str] = "https://api.ejemplo.com"

class ClienteApi:
    url: Final[str]
    _cabecera_version: Final[str] = "X-Api-Version"

    def __init__(self, url: str = URL_PREDETERMINADA) -> None:
        self.url = url.rstrip("/")

    def obtener(self, ruta: str) -> bytes:
        return self._enviar("GET", ruta)

    @final
    def _enviar(self, metodo: str, ruta: str) -> bytes:
        headers = {self._cabecera_version: "1"}
        return transporte_http(
            metodo,
            f"{self.url}/{ruta.lstrip('/')}",
            headers=headers,
        )

class ClienteConCache(ClienteApi):
    def obtener(self, ruta: str) -> bytes:
        if datos := cache_obtener(ruta):
            return datos
        datos = super().obtener(ruta)
        cache_guardar(ruta, datos)
        return datos

La URL de instancia se asigna una vez, el nombre de cabecera es fijo y el envío no puede sobrescribirse. La operación de alto nivel sigue siendo extensible.

Probar el contrato

Ejecuta un checker en CI y conserva fixtures con reasignaciones y sobrescrituras inválidas. Las pruebas de runtime deben verificar inmutabilidad real solo cuando está implementada mediante tuplas, propiedades, dataclasses congeladas u otros mecanismos concretos.

Conclusión

typing.Final protege nombres y atributos contra reasignación en el contrato estático, mientras @final impide sobrescritura y herencia para analizadores. Ambas herramientas documentan invariantes y reducen extensiones accidentales.

La documentación oficial de Final y del decorador final en Python define la semántica. Combínalos con estructuras inmutables o controles de runtime cuando la regla deba aplicarse realmente.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Código en una pantalla que representa t-strings en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    T-strings en Python 3.14: templates estructurados

    Aprende t-strings en Python 3.14, explora Template e Interpolation y crea renderizadores estructurados, extensibles y seguros.

    Ler mais

    Tempo de leitura: 6 minutos
    16/10/2026
    CPU y módulos de memoria que representan shared_memory track en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    shared_memory track: controla el resource tracker

    Aprende shared_memory track en Python para controlar el resource tracker, la propiedad y la limpieza entre procesos.

    Ler mais

    Tempo de leitura: 5 minutos
    16/10/2026
    Unidad de almacenamiento de datos que representa compression.zstd en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: streaming y compresión segura

    Aprende compression.zstd en Python para comprimir archivos y datos con velocidad, streaming, diccionarios y límites seguros.

    Ler mais

    Tempo de leitura: 7 minutos
    15/10/2026
    Código en pantalla para diagnosticar procesos con sys.remote_exec en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sys.remote_exec: diagnostica procesos Python

    Aprende sys.remote_exec en Python para diagnosticar procesos activos con scripts controlados, permisos seguros y buenas prácticas.

    Ler mais

    Tempo de leitura: 7 minutos
    15/10/2026
    Código Python y configuración de variables de entorno con os.reload_environ
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    os.reload_environ: threads y variables de entorno

    Aprende os.reload_environ en Python para actualizar variables de entorno externas con validación, compatibilidad y precauciones de concurrencia.

    Ler mais

    Tempo de leitura: 5 minutos
    14/10/2026
    Código Python que representa contextvars.Token y aislamiento de contexto asíncrono
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    contextvars.Token: restaura contexto async

    Aprende contextvars.Token en Python para valores temporales, restauración automática y aislamiento seguro entre tareas asíncronas.

    Ler mais

    Tempo de leitura: 7 minutos
    14/10/2026