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 ConsultaSQLLa 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 selfUna 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.5Esto 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 actualLa 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 selfEn 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 selfSelf 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 nuevoEl 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 SelfLas 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.







