typing.Self en Python: retornos fluidos

Publicado el: 28/08/2026
Tempo de leitura: 5 minutos
A developer typing code on a laptop with a Python book beside in an office.

Los métodos que devuelven su propia instancia son frecuentes en builders, APIs fluidas, objetos configurables y context managers. Anotar esos retornos con el nombre de la clase funciona hasta que aparece una subclase: el analizador puede perder el subtipo más específico. typing.Self significa “la clase concreta actual” y conserva el tipo de las subclases sin declarar manualmente un TypeVar ligado.

Esta guía explica Self en métodos de instancia, classmethods, constructores alternativos, copias, context managers, protocolos, clases genéricas y herencia. También muestra cuándo Self no es la anotación correcta.

El problema de devolver el nombre de la clase

class Consulta:
    def limitar(self, cantidad: int) -> "Consulta":
        self._limite = cantidad
        return self

class ConsultaSQL(Consulta):
    def ordenar(self, campo: str) -> "ConsultaSQL":
        self._orden = campo
        return self

consulta = ConsultaSQL().limitar(10)
# El analizador puede ver Consulta, no ConsultaSQL

La implementación devuelve la misma instancia concreta, pero la anotación fija promete únicamente Consulta. El encadenamiento puede perder métodos exclusivos de la subclase.

Usar Self

from typing import Self

class Consulta:
    def limitar(self, cantidad: int) -> Self:
        self._limite = cantidad
        return self

class ConsultaSQL(Consulta):
    def ordenar(self, campo: str) -> Self:
        self._orden = campo
        return self

consulta = ConsultaSQL().limitar(10).ordenar("nombre")

Self se interpreta según el tipo concreto del receptor, por lo que el resultado de limitar() sigue siendo ConsultaSQL.

APIs fluidas

Los builders suelen modificar el objeto y devolver self.

class Solicitud:
    def __init__(self) -> None:
        self._headers: dict[str, str] = {}
        self._timeout = 5.0

    def header(self, nombre: str, valor: str) -> Self:
        self._headers[nombre] = valor
        return self

    def timeout(self, segundos: float) -> Self:
        if segundos <= 0:
            raise ValueError("el timeout debe ser positivo")
        self._timeout = segundos
        return self

Una subclase puede añadir métodos y conservarlos después de las llamadas fluidas heredadas.

Self en classmethods

Self también describe una factory que devuelve una instancia de la clase sobre la que se invocó.

class Documento:
    def __init__(self, texto: str) -> None:
        self.texto = texto

    @classmethod
    def vacio(cls) -> Self:
        return cls("")

class DocumentoMarkdown(Documento):
    pass

markdown = DocumentoMarkdown.vacio()

El tipo inferido de markdown es DocumentoMarkdown, siempre que la implementación construya realmente cls.

Factory que siempre devuelve la clase base

No uses Self si el método construye una clase específica sin importar la subclase.

class Documento:
    @classmethod
    def predeterminado(cls) -> "Documento":
        return Documento("plantilla")

Anotarlo con Self prometería falsamente que DocumentoMarkdown.predeterminado() devuelve un DocumentoMarkdown.

Constructores alternativos

class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    @classmethod
    def origen(cls) -> Self:
        return cls(0.0, 0.0)

    @classmethod
    def desde_iterable(cls, valores) -> Self:
        x, y = valores
        return cls(float(x), float(y))

Las subclases deben mantener un constructor compatible o sobrescribir la factory. Self describe la relación de retorno, pero no garantiza en runtime que todos los constructores acepten los mismos argumentos.

Métodos de copia y clone

from copy import copy

class Configuracion:
    def clonar(self) -> Self:
        return copy(self)

Si copy(self) conserva la clase concreta, Self describe correctamente el resultado. Self expresa el tipo, no la identidad del objeto.

Self en parámetros

Self puede aparecer en un parámetro cuando la operación necesita otro objeto del mismo tipo concreto.

class Punto:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def distancia_a(self, otro: Self) -> float:
        dx = self.x - otro.x
        dy = self.y - otro.y
        return (dx * dx + dy * dy) ** 0.5

Esto es más restrictivo que aceptar cualquier Punto. Usa Self solo cuando la relación entre receptor y argumento sea real. Si las subclases pueden interactuar con la clase base, anota la clase base.

Self en propiedades

Una propiedad puede devolver una instancia de la misma clase concreta.

class Nodo:
    @property
    def raiz(self) -> Self:
        actual = self
        while actual.padre is not None:
            actual = actual.padre
        return actual

La anotación solo es correcta si todos los padres de la cadena tienen el mismo tipo concreto. En árboles heterogéneos, puede ser necesario devolver la clase base.

Context managers

__enter__ normalmente devuelve self.

class Sesion:
    def __enter__(self) -> Self:
        self.abrir()
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.cerrar()

class SesionAuditada(Sesion):
    def evento(self, texto: str) -> None:
        ...

with SesionAuditada() as sesion:
    sesion.evento("inicio")

Self conserva SesionAuditada dentro del bloque.

Self en Protocol

Un protocolo puede exigir comportamiento fluido.

from typing import Protocol, Self

class Configurable(Protocol):
    def configurar(self, clave: str, valor: object) -> Self:
        ...

Una implementación compatible debe devolver su propio tipo concreto. La guía de Protocol en Python explica el tipado estructural.

Self en clases genéricas

Self representa la clase concreta junto con su especialización genérica.

from typing import Generic, TypeVar, Self

T = TypeVar("T")

class Caja(Generic[T]):
    def __init__(self, valor: T) -> None:
        self.valor = valor

    def reemplazar(self, valor: T) -> Self:
        self.valor = valor
        return self

En Caja[int], el método devuelve la misma especialización.

Cuando cambia el parámetro genérico

Self no sirve si el retorno posee parámetros de tipo diferentes.

from collections.abc import Callable

U = TypeVar("U")

class Caja(Generic[T]):
    def mapear(self, funcion: Callable[[T], U]) -> "Caja[U]":
        return Caja(funcion(self.valor))

mapear() crea una Caja[U], no necesariamente el mismo tipo de self. Los genéricos explícitos describen mejor la transformación.

Comparación con TypeVar bound

Antes de Self, era habitual usar un TypeVar ligado al receptor.

from typing import TypeVar

TConsulta = TypeVar("TConsulta", bound="Consulta")

class Consulta:
    def limitar(self: TConsulta, cantidad: int) -> TConsulta:
        self._limite = cantidad
        return self

Self es más breve y claro para este patrón. TypeVar sigue siendo útil cuando la relación abarca varias funciones, parámetros o tipos externos.

Las subclases deben respetar el contrato

class Base:
    def normalizar(self) -> Self:
        return self

class Especial(Base):
    def normalizar(self) -> Base:
        return Base()

Un analizador debería marcar la sobrescritura incompatible. Un método base con Self promete que las subclases devuelven su propio tipo concreto.

Self no significa el mismo objeto

from copy import copy

class Registro:
    def con_nombre(self, nombre: str) -> Self:
        nuevo = copy(self)
        nuevo.nombre = nombre
        return nuevo

El método devuelve un objeto nuevo del mismo tipo concreto. Documenta si una operación fluida modifica el receptor o crea una copia.

Decoradores y preservación de firmas

Un decorador mal tipado puede borrar la relación de Self. Usa functools.wraps en runtime y ParamSpec o genéricos precisos cuando el decorador deba conservar parámetros y retorno.

Un decorador que devuelve Callable[..., object] elimina la información útil aunque el método siga funcionando.

Compatibilidad de versión

typing.Self está disponible en Python moderno. Para versiones anteriores, usa typing_extensions.Self.

try:
    from typing import Self
except ImportError:
    from typing_extensions import Self

Las bibliotecas deben declarar la dependencia y probar todas las versiones soportadas.

Errores comunes

  • Usar Self en una factory que construye la clase base: la promesa es falsa.
  • Usar Self cuando cambia el parámetro genérico: declara el tipo de destino.
  • Restringir parámetros sin necesidad: quizá la clase base sea correcta.
  • Suponer que Self significa identidad: un objeto nuevo del mismo subtipo es válido.
  • Ignorar constructores incompatibles: cls(...) puede fallar en runtime.
  • Usar Self en un staticmethod: no existe un receptor que determine el tipo concreto.

Ejemplo completo: builder de consultas

from typing import Self

class Query:
    def __init__(self, tabla: str) -> None:
        self.tabla = tabla
        self.filtros: list[str] = []
        self._limite: int | None = None

    def donde(self, expresion: str) -> Self:
        self.filtros.append(expresion)
        return self

    def limitar(self, cantidad: int) -> Self:
        if cantidad < 1:
            raise ValueError("límite inválido")
        self._limite = cantidad
        return self

    @classmethod
    def desde_tabla(cls, tabla: str) -> Self:
        return cls(tabla)

class QueryOrdenada(Query):
    def ordenar(self, campo: str) -> Self:
        self.orden = campo
        return self

consulta = (
    QueryOrdenada.desde_tabla("clientes")
    .donde("activo = true")
    .limitar(20)
    .ordenar("nombre")
)

Cada método conserva QueryOrdenada, incluida la factory heredada.

Cuándo evitar una API fluida

Self mejora el tipado, pero no garantiza que el encadenamiento sea el diseño más claro. Cadenas largas pueden ocultar efectos secundarios y dificultar el debugging. Métodos que realizan I/O, persisten datos o cambian estado global suelen ser más claros con resultados explícitos.

Usa fluidez para configuración y transformaciones previsibles, y documenta la mutabilidad y las fallas.

Conclusión

typing.Self representa la clase concreta del receptor. Simplifica métodos fluidos, constructores alternativos, clones, context managers, protocolos y builders, conservando automáticamente las subclases.

La documentación oficial de Self en Python detalla los usos permitidos. Elige Self cuando el retorno o parámetro siga realmente el tipo concreto, y usa genéricos explícitos o la clase base cuando la relación sea diferente.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ExceptionGroup en Python: múltiples errores

    Aprende ExceptionGroup en Python para múltiples errores, except*, grupos anidados, TaskGroup, filtros, logging y validación por lotes.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Picturesque wooden boardwalk leading to a serene beach under clear blue skies.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk en Python: recorre directorios

    Aprende Path.walk en Python para recorrer directorios, podar carpetas, manejar errores y symlinks, calcular tamaños y soportar versiones antiguas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A man in a blue shirt holding a wall clock above his head, contemplating time.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.timeout en Python: controla plazos

    Aprende asyncio.timeout en Python para deadlines, timeout_at, reagendamiento, TaskGroup, cleanup, retries y cancelación asíncrona segura.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Detailed view of a resting reticulated python showcasing its textured scales and intricate patterns.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TaskGroup en Python: concurrencia estructurada

    Aprende asyncio.TaskGroup en Python para concurrencia estructurada, resultados, cancelación, ExceptionGroup, timeouts y grupos anidados.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A monochrome image of a lens on an open dictionary page, highlighting words.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    MappingProxyType: diccionario solo lectura

    Aprende MappingProxyType en Python para exponer diccionarios de solo lectura, crear vistas dinámicas y snapshots, y proteger invariantes sin copias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Retro fuel pump with rusty metal and vintage design, featuring a nozzle and liter meter.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Literal en Python: restringe valores

    Aprende typing.Literal en Python para restringir valores, crear overloads, discriminar TypedDict, usar match/case y mejorar APIs tipadas.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026