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

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Annotated en Python: tipos con metadatos

    Aprende Annotated en Python para añadir metadatos a tipos, crear validación, schemas, unidades e integraciones con frameworks.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    NewType en Python: separa identificadores

    Aprende NewType en Python para separar IDs, códigos y valores primitivos, validar fronteras y evitar mezclas de dominio.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Vivid close-up of code on a computer screen showcasing programming details.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Never en Python: marca código inalcanzable

    Aprende typing.Never en Python para funciones sin retorno, código inalcanzable y exhaustividad con assert_never, Literal y Enum.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate en Python: cambia parámetros

    Aprende Concatenate en Python para añadir u ocultar parámetros iniciales en decoradores tipados con ParamSpec y dependencias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec en Python: conserva firmas

    Aprende ParamSpec en Python para conservar firmas completas en decoradores, callbacks, wrappers async y funciones de orden superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up of a python snake curled up, showcasing its detailed scales.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs en Python: refina ambas ramas

    Aprende TypeIs en Python para refinar las ramas verdadera y falsa, compararlo con TypeGuard y crear predicados de tipo seguros.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026