Métodos especiales en Python: el protocolo dunder completo

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 22 min de lectura

Cuando escribes len(mi_lista), Python no invoca un método mágico escondido en el intérprete. Invoca mi_lista.__len__(). Cuando sumas dos objetos con +, Python llama a __add__. Cuando entras en un bloque with, Python llama a __enter__. Los métodos especiales son el protocolo que permite que tus propias clases se integren con el lenguaje de forma tan natural como los tipos incorporados.

Esto es lo que los desarrolladores de Python denominan el "protocolo de datos": un contrato entre tu objeto y el intérprete. Si tu objeto habla el protocolo, Python lo trata como ciudadano de primera clase. Si no lo habla, te quedas con objetos que solo funcionan con sus propios métodos, sin integración con el ecosistema.

🪪 __repr__ y __str__: la identidad del objeto

Lo primero que cualquier objeto debería tener es una representación decente. Cuando haces debug, cuando imprimes resultados en un REPL, cuando registras un error en un log, Python necesita convertir tu objeto en texto. Si no le dices cómo hacerlo, obtienes cosas como <MiClase object at 0x7f3b2a4c1d50>, que no ayuda a nadie.

class Temperatura:
    def __init__(self, celsius: float):
        self._celsius = celsius

    def __repr__(self) -> str:
        # Para desarrolladores: recreable, preciso
        return f"Temperatura(celsius={self._celsius!r})"

    def __str__(self) -> str:
        # Para usuarios: legible, amable
        return f"{self._celsius}°C"

    def __format__(self, spec: str) -> str:
        # Para f-strings y format(): f"{t:.1f}" → "20.5°C"
        return f"{self._celsius:{spec}}°C"

t = Temperatura(20.5)
print(repr(t))          # Temperatura(celsius=20.5)  ← __repr__
print(str(t))           # 20.5°C                     ← __str__
print(f"{t:.1f}")       # 20.5°C                     ← __format__
print(f"{t!r}")         # Temperatura(celsius=20.5)  ← fuerza __repr__
🔵 Regla de oro
Implementa siempre __repr__. Añade __str__ solo si quieres una presentación diferente para el usuario final. Python usa __repr__ como fallback cuando no hay __str__, pero nunca al revés.
Dos adaptadores de viaje universales mostrando conectores para enchufes UK, Europa y USA
Un adaptador universal tiene conectores estandarizados que encajan en cualquier toma del mundo. Los métodos especiales funcionan igual: tu objeto implementa el conector correcto (__len__, __add__, __iter__) y Python lo reconoce nativamente, igual que si fuera una lista o un número incorporado. Fuente: Depositphotos.

⚖️ __eq__, __lt__ y @total_ordering

Por defecto, Python compara objetos por identidad (a is b). Si quieres comparación por valor — que es lo que casi siempre quieres — tienes que definirla explícitamente. Y si defines __eq__, tienes que pensar también en __hash__.

from functools import total_ordering

@total_ordering
class Temperatura:
    def __init__(self, celsius: float):
        self._celsius = celsius

    def __eq__(self, otro) -> bool:
        if not isinstance(otro, Temperatura):
            return NotImplemented   # ← NotImplemented, no raise!
        return self._celsius == otro._celsius

    def __lt__(self, otro) -> bool:
        if not isinstance(otro, Temperatura):
            return NotImplemented
        return self._celsius < otro._celsius

    def __hash__(self) -> int:
        return hash(self._celsius)   # necesario porque definimos __eq__

t1 = Temperatura(20)
t2 = Temperatura(30)
t3 = Temperatura(20)

print(t1 == t3)          # True
print(t1 < t2)           # True
print(t1 > t2)           # False  ← @total_ordering lo infiere
print(t1 <= t3)          # True   ← @total_ordering lo infiere
print(sorted([t2, t1]))  # [Temperatura(20), Temperatura(30)]
print({t1, t3})          # {Temperatura(20)}  — __hash__ funciona
💡 @total_ordering
Con solo __eq__ + __lt__, el decorador @total_ordering infiere automáticamente __le__, __gt__ y __ge__. Ahorra repetición y garantiza consistencia. El único coste: una pequeña penalización de rendimiento en comparaciones muy frecuentes.

➕ Operadores aritméticos: __add__, __mul__ y reflejos

Los operadores aritméticos en Python invocan métodos especiales en el operando izquierdo. Si ese método no sabe cómo operar con el tipo del operando derecho y retorna NotImplemented, Python prueba el método "reflejo" en el operando derecho.

class Vector2D:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Vector2D({self.x}, {self.y})"

    def __add__(self, otro):
        if isinstance(otro, Vector2D):
            return Vector2D(self.x + otro.x, self.y + otro.y)
        return NotImplemented   # no raise, retornar NotImplemented

    def __radd__(self, otro):   # para: escalar + vector
        return self.__add__(otro) if isinstance(otro, Vector2D) else NotImplemented

    def __mul__(self, escalar: float):
        if isinstance(escalar, (int, float)):
            return Vector2D(self.x * escalar, self.y * escalar)
        return NotImplemented

    def __rmul__(self, escalar: float):   # para: 3 * vector
        return self.__mul__(escalar)

    def __neg__(self):
        return Vector2D(-self.x, -self.y)

    def __abs__(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector2D(1, 2)
v2 = Vector2D(3, 4)
print(v1 + v2)    # Vector2D(4, 6)
print(v1 * 3)     # Vector2D(3, 6)
print(3 * v1)     # Vector2D(3, 6)  ← __rmul__
print(-v1)        # Vector2D(-1, -2)
print(abs(v2))    # 5.0
Diagrama radial de métodos especiales en Python agrupados por categoría: representación, comparación, aritmética, contenedor, invocable, gestor de contexto, atributos dinámicos y conversión numérica, con el nodo central Tu clase Python nativo
El protocolo dunder completo: cada categoría de métodos especiales conecta tu clase al nodo central de Python nativo. Representación (__repr__, __str__, __hash__), Comparación (__eq__, __lt__, __le__), Aritmética (__add__, __mul__, __iadd__ con reflejos), Contenedor (__len__, __getitem__, __iter__), Invocable (__call__, __new__, __init__), Gestor de contexto (__enter__, __exit__), Atributos dinámicos (__getattr__, __setattr__) y Conversión numérica (__int__, __float__, __abs__). Infografía: Ciberaula.

📦 Protocolo contenedor: __len__, __getitem__, __iter__

Para que Python trate tu objeto como una secuencia o contenedor — iterable con for, indexable con [], medible con len() — tienes que hablar el protocolo contenedor. No hace falta heredar de ninguna clase: basta con implementar los métodos correctos.

class ListaOrdenada:
    """Contenedor que mantiene sus elementos siempre ordenados."""

    def __init__(self, iterable=None):
        self._data = sorted(iterable or [])

    def añadir(self, valor) -> None:
        import bisect
        bisect.insort(self._data, valor)

    def __len__(self) -> int:
        return len(self._data)

    def __getitem__(self, indice):
        return self._data[indice]   # soporta slices automáticamente

    def __contains__(self, valor) -> bool:
        import bisect
        i = bisect.bisect_left(self._data, valor)
        return i < len(self._data) and self._data[i] == valor

    def __iter__(self):
        return iter(self._data)

    def __repr__(self) -> str:
        return f"ListaOrdenada({self._data!r})"

ls = ListaOrdenada([5, 2, 8, 1])
print(ls)           # ListaOrdenada([1, 2, 5, 8])
ls.añadir(3)
print(len(ls))      # 5
print(ls[0])        # 1
print(ls[1:3])      # [2, 3]   ← slice funciona gratis
print(3 in ls)      # True     ← __contains__ O(log n)
for x in ls:
    print(x, end=" ")   # 1 2 3 5 8
🔵 Iteración sin __iter__
Si implementas __getitem__ con índices enteros desde 0, Python puede iterar tu objeto automáticamente aunque no definas __iter__: probará índices 0, 1, 2... hasta que obtenga IndexError. Pero definir __iter__ explícitamente es más eficiente y más claro.

📞 __call__: objetos que se comportan como funciones

Cuando defines __call__, tu objeto se vuelve invocable: obj(args) llama a obj.__call__(args). Esto es perfecto para callbacks configurables, memoización, o cualquier función que necesite estado persistente entre invocaciones.

from functools import wraps
from typing import Any

class Memoize:
    """Decorator que memoriza los resultados de llamadas previas."""

    def __init__(self, func):
        self._func   = func
        self._cache  = {}
        self._hits   = 0
        self._misses = 0
        wraps(func)(self)   # copia __name__, __doc__, etc.

    def __call__(self, *args, **kwargs):
        clave = (args, tuple(sorted(kwargs.items())))
        if clave not in self._cache:
            self._misses += 1
            self._cache[clave] = self._func(*args, **kwargs)
        else:
            self._hits += 1
        return self._cache[clave]

    def __repr__(self) -> str:
        return f"Memoize({self._func.__name__!r}, hits={self._hits}, misses={self._misses})"

    def limpiar_cache(self) -> None:
        self._cache.clear()
        self._hits = self._misses = 0

@Memoize
def fibonacci(n: int) -> int:
    if n < 2: return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(35))    # 9227465 — rápido gracias a la memoización
print(fibonacci)        # Memoize('fibonacci', hits=68, misses=36)
Mano estampando un sello de goma sobre un documento en un escritorio de oficina
Un sello de goma imprime una marca oficial e inconfundible en cada documento que toca. El método __repr__ hace lo mismo: imprime la «huella» oficial del objeto en cada salida de debug, en cada log, en cada traza de error. Sin él, todos los objetos parecen anónimos. Fuente: Depositphotos.

🔒 Context managers: __enter__ y __exit__

Los context managers son la solución elegante de Python para recursos que necesitan ser liberados pase lo que pase: conexiones a bases de datos, ficheros, locks, transacciones. La sentencia with garantiza que __exit__ siempre se ejecuta, incluso si ocurre una excepción.

import time

class Cronometro:
    """Mide el tiempo de un bloque de código con with."""

    def __enter__(self):
        self._inicio = time.perf_counter()
        return self   # lo que retorna __enter__ va a la variable "as"

    def __exit__(self, tipo_exc, valor_exc, traceback):
        self.elapsed = time.perf_counter() - self._inicio
        if tipo_exc is not None:
            print(f"⚠️  Excepción tras {self.elapsed:.4f}s: {valor_exc}")
        return False   # False → propaga la excepción; True → la suprime

    def __repr__(self) -> str:
        return f"Cronometro(elapsed={getattr(self, 'elapsed', None)!r})"

with Cronometro() as c:
    total = sum(i**2 for i in range(1_000_000))

print(f"Resultado: {total:,}")
print(f"Tiempo:    {c.elapsed:.4f}s")
# Alternativa con contextlib.contextmanager (más conciso)
from contextlib import contextmanager

@contextmanager
def cronometro():
    inicio = time.perf_counter()
    try:
        yield   # aquí se ejecuta el cuerpo del with
    finally:
        elapsed = time.perf_counter() - inicio
        print(f"Tiempo: {elapsed:.4f}s")

with cronometro():
    total = sum(i**2 for i in range(1_000_000))
💡 ¿Cuándo usar la clase vs contextlib?
Usa la clase cuando el context manager tiene estado que quieres acceder después del bloque (c.elapsed), o cuando forma parte de una clase más grande. Usa @contextmanager cuando es un helper puntual: el código es más compacto y fácil de leer.
Medallón de Aristóteles, filósofo griego
Anima est quodammodo omnia
«El alma es, en cierta manera, todas las cosas»
Aristóteles · De Anima, Libro III · s. IV a.C.
Una clase que implementa el protocolo dunder completo puede ser todas las cosas para Python: un número, un contenedor, una función, un gestor de contexto. Aristóteles observó que el alma humana puede conocer todas las formas al adoptarlas en el pensamiento. Tu clase puede «ser» cualquier cosa que Python entienda, siempre que hable el protocolo correcto. No heredes de todo — implementa el contrato.

🔀 Control de atributos: __getattr__ y __setattr__

Python te permite interceptar el acceso a atributos. __getattr__ se llama solo cuando Python no encuentra el atributo por los medios normales. __setattr__ se llama en cada asignación. Y __getattribute__ — el más potente y peligroso — se llama en cada acceso, sin excepción.

class ConfiguracionFlexible:
    """Acceso a configuración mediante atributos dinámicos."""

    def __init__(self, **kwargs):
        # ¡Usar object.__setattr__ para evitar recursión!
        object.__setattr__(self, '_datos', dict(kwargs))
        object.__setattr__(self, '_cambios', [])

    def __getattr__(self, nombre: str):
        # Solo se llama si Python no encuentra el atributo normalmente
        datos = object.__getattribute__(self, '_datos')
        if nombre in datos:
            return datos[nombre]
        raise AttributeError(f"Configuración sin clave '{nombre}'")

    def __setattr__(self, nombre: str, valor):
        # Se llama en CADA asignación — evitar recursión con object.__setattr__
        datos  = object.__getattribute__(self, '_datos')
        cambios = object.__getattribute__(self, '_cambios')
        cambios.append((nombre, datos.get(nombre), valor))
        datos[nombre] = valor

    def __repr__(self) -> str:
        datos = object.__getattribute__(self, '_datos')
        return f"ConfiguracionFlexible({datos!r})"

cfg = ConfiguracionFlexible(host="localhost", puerto=5432)
print(cfg.host)     # "localhost"   ← __getattr__
cfg.debug = True    # __setattr__ registra el cambio
print(cfg.debug)    # True
⚠️ Recursión infinita en __setattr__ y __getattribute__
El error más común: dentro de __setattr__, hacer self.algo = valor vuelve a llamar a __setattr__ → recursión infinita. Siempre usa object.__setattr__(self, nombre, valor) para asignaciones internas. Lo mismo aplica a __getattribute__: usa object.__getattribute__(self, nombre) para acceder a atributos internos.

🛠️ Programa completo: Vector2D con protocolo completo

from functools import total_ordering
import math

@total_ordering
class Vector2D:
    """Vector 2D con protocolo dunder completo."""

    def __init__(self, x: float, y: float):
        self._x = float(x)
        self._y = float(y)

    # ── Propiedades ──────────────────────────────────────────
    @property
    def x(self) -> float: return self._x

    @property
    def y(self) -> float: return self._y

    # ── Representación ───────────────────────────────────────
    def __repr__(self) -> str:
        return f"Vector2D({self._x}, {self._y})"

    def __str__(self) -> str:
        return f"({self._x:.2f}, {self._y:.2f})"

    def __format__(self, spec: str) -> str:
        return f"({self._x:{spec}}, {self._y:{spec}})"

    # ── Comparación ──────────────────────────────────────────
    def __eq__(self, otro) -> bool:
        if not isinstance(otro, Vector2D): return NotImplemented
        return math.isclose(self._x, otro._x) and math.isclose(self._y, otro._y)

    def __lt__(self, otro) -> bool:
        if not isinstance(otro, Vector2D): return NotImplemented
        return abs(self) < abs(otro)

    def __hash__(self) -> int:
        return hash((round(self._x, 9), round(self._y, 9)))

    def __bool__(self) -> bool:
        return self._x != 0 or self._y != 0

    # ── Aritmética ───────────────────────────────────────────
    def __add__(self, otro):
        if isinstance(otro, Vector2D):
            return Vector2D(self._x + otro._x, self._y + otro._y)
        return NotImplemented

    def __radd__(self, otro): return self.__add__(otro)

    def __sub__(self, otro):
        if isinstance(otro, Vector2D):
            return Vector2D(self._x - otro._x, self._y - otro._y)
        return NotImplemented

    def __mul__(self, escalar):
        if isinstance(escalar, (int, float)):
            return Vector2D(self._x * escalar, self._y * escalar)
        if isinstance(escalar, Vector2D):   # producto escalar
            return self._x * escalar._x + self._y * escalar._y
        return NotImplemented

    def __rmul__(self, escalar): return self.__mul__(escalar)

    def __truediv__(self, escalar):
        if not isinstance(escalar, (int, float)):
            return NotImplemented
        if escalar == 0:
            raise ZeroDivisionError("No se puede dividir un vector por cero.")
        return Vector2D(self._x / escalar, self._y / escalar)

    def __neg__(self): return Vector2D(-self._x, -self._y)

    def __pos__(self): return Vector2D(+self._x, +self._y)

    def __abs__(self) -> float:
        return math.hypot(self._x, self._y)

    def __round__(self, n: int = 0):
        return Vector2D(round(self._x, n), round(self._y, n))

    # ── Contenedor / Secuencia ───────────────────────────────
    def __len__(self) -> int:
        return 2

    def __getitem__(self, indice: int) -> float:
        return (self._x, self._y)[indice]

    def __iter__(self):
        yield self._x
        yield self._y

    def __contains__(self, valor: float) -> bool:
        return math.isclose(valor, self._x) or math.isclose(valor, self._y)

    # ── Conversión ───────────────────────────────────────────
    def __int__(self)   -> int:   return int(abs(self))
    def __float__(self) -> float: return abs(self)
    def __complex__(self) -> complex: return complex(self._x, self._y)

    # ── Contexto ─────────────────────────────────────────────
    def __enter__(self): return self

    def __exit__(self, *_): return False


# ── Uso ──────────────────────────────────────────────────────
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)

print(repr(v1))             # Vector2D(3.0, 4.0)
print(str(v1))              # (3.00, 4.00)
print(f"{v1:.3f}")          # (3.000, 4.000)
print(v1 + v2)              # Vector2D(4.0, 6.0)
print(v1 * 2)               # Vector2D(6.0, 8.0)
print(3 * v1)               # Vector2D(9.0, 12.0)
print(v1 * v2)              # 11.0  ← producto escalar
print(abs(v1))              # 5.0
print(-v1)                  # Vector2D(-3.0, -4.0)
print(len(v1))              # 2
print(list(v1))             # [3.0, 4.0]
print(v1[0], v1[1])         # 3.0 4.0
print(3.0 in v1)            # True
print(v1 == Vector2D(3,4))  # True
print(v1 < Vector2D(10,0))  # True  (|v1|=5 < 10)
print(sorted([v1, v2]))     # [Vector2D(1.0, 2.0), Vector2D(3.0, 4.0)]
print({v1, v2})             # funciona ← __hash__

with v1 as v:
    x, y = v
    print(f"x={x}, y={y}")  # x=3.0, y=4.0

🐛 Errores clásicos con métodos especiales

1. raise NotImplementedError en operadores (en lugar de return NotImplemented)

# ❌ Rompe el protocolo reflejo
def __add__(self, otro):
    if not isinstance(otro, MiClase):
        raise NotImplementedError   # Python se detiene aquí, __radd__ nunca se prueba

# ✅
def __add__(self, otro):
    if not isinstance(otro, MiClase):
        return NotImplemented   # Python prueba otro.__radd__(self)

2. __add__ que modifica self en lugar de retornar objeto nuevo

# ❌ Viola la semántica de inmutabilidad esperada en operadores
def __add__(self, otro):
    self.x += otro.x   # mutar self es un horror silencioso
    return self

# ✅ Siempre retornar un objeto NUEVO
def __add__(self, otro):
    return Vector2D(self.x + otro.x, self.y + otro.y)

3. __eq__ sin __hash__ (objeto se vuelve inutilizable en dicts/sets)

# ❌ Al definir __eq__, Python pone __hash__ = None automáticamente
class Punto:
    def __eq__(self, otro): return self.x == otro.x and self.y == otro.y
    # sin __hash__ → TypeError al usar en dict o set

# ✅
class Punto:
    def __eq__(self, otro): return self.x == otro.x and self.y == otro.y
    def __hash__(self): return hash((self.x, self.y))

4. Recursión infinita en __setattr__ o __getattribute__

# ❌ Recursión infinita
def __setattr__(self, nombre, valor):
    self._log.append(nombre)   # ← llama a __setattr__ de nuevo → recursión

# ✅ Usar object.__setattr__ para acceso interno
def __setattr__(self, nombre, valor):
    log = object.__getattribute__(self, '_log')
    log.append(nombre)
    object.__setattr__(self, nombre, valor)

✅ Resumen y próximos pasos

Los métodos especiales son el protocolo que hace que tus clases sean ciudadanos de primera clase en Python. __repr__ siempre; __str__ cuando quieres presentación diferente. __eq__ implica __hash__. Usa @total_ordering para no repetir comparaciones. Retorna NotImplemented (no raises) en operadores. __call__ convierte objetos en funciones con estado. __enter__/__exit__ garantiza limpieza de recursos. __getattr__ es el último recurso; __getattribute__ intercepta absolutamente todo.

La siguiente lección: clases abstractas — el mecanismo formal de Python para definir interfaces y obligar a las subclases a implementar métodos concretos.

Ficha de referencia de métodos especiales Python: __repr__ y __str__, __bool__ y __hash__, __call__ invocable, comparación __eq__ y __lt__, aritmética __add__ __mul__ con reflejos, negación abs round, contenedor __len__ __getitem__, iteración __iter__, context manager __enter__ __exit__, errores frecuentes, operadores reflejos y tabla de decisión
Referencia rápida — __repr__/__str__ (debug vs usuario); __bool__/__hash__ (falseable y usable como clave); __call__ con ejemplo Multiplicador; Comparación con __eq__/__lt__ y @total_ordering; Aritmética con __add__/__mul__ y operadores reflejos __rmul__; __neg__/__abs__/__round__; Contenedor __len__/__getitem__; Iteración __iter__/__contains__; Context manager Cronometro con __enter__/__exit__; Errores frecuentes y tabla de decisión. Ficha: Ciberaula.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Métodos especiales en Python: el protocolo dunder completo

Las dudas más comunes respondidas de forma clara y directa.

__repr__ es para desarrolladores: debe devolver una cadena que, idealmente, permita recrear el objeto (eval(repr(obj)) == obj). Piensa en él como el "modo debug". __str__ es para usuarios finales: puede ser más legible, más amable. Si solo implementas uno, implementa __repr__: Python usa __repr__ como fallback cuando no hay __str__, pero no al revés. Regla práctica: empieza siempre por __repr__; añade __str__ solo si quieres una presentación diferente para el usuario final.
Son dos cosas distintas. NotImplemented es un valor de retorno especial (singleton) que le dice a Python: "No sé cómo hacer esta operación con estos tipos, prueba con el operando derecho". Python entonces intentará __radd__, __rmul__, etc. Si retornas NotImplemented, Python sigue intentando. Si lanzas NotImplementedError (una excepción), Python se detiene inmediatamente con error. En operadores matemáticos, casi siempre quieres retornar NotImplemented para que el protocolo reflejo funcione correctamente.
Es un decorador de clase que te permite implementar solo __eq__ y un método de comparación (__lt__, __le__, __gt__ o __ge__), y él infiere automáticamente los demás. Por ejemplo, si implementas __eq__ y __lt__, Python puede deducir que a > b equivale a b < a, y que a <= b equivale a a == b or a < b. Existe porque implementar los 6 métodos de comparación a mano es tedioso y propenso a inconsistencias. La pequeña desventaja es que puede ser un poco más lento que implementarlos todos manualmente.
Cuando necesitas un objeto que se comporte como una función pero que tenga estado persistente entre llamadas. Casos de uso típicos: callbacks configurables con parámetros fijos (como un multiplicador con factor configurable), memoización personalizada, decoradores con estado, o cualquier situación donde necesitas que una función "recuerde" algo entre invocaciones. La diferencia con una closure es que __call__ en una clase es más explícito, más fácil de hacer picklable y más fácil de testear.
Si __exit__ lanza una excepción, esa nueva excepción reemplaza a cualquier excepción que hubiera en el bloque with. Esto puede ocultar el error original, lo cual es casi siempre un comportamiento no deseado. La buena práctica es que __exit__ capture y registre los errores internos de limpieza, pero no los propague como excepciones. Si __exit__ retorna True, suprime la excepción del bloque with. Si retorna False o None, la excepción del bloque with se propaga normalmente.
__getattr__ solo se llama cuando Python no encuentra el atributo por los medios normales (no está en __dict__ ni en la clase). Es el "último recurso". __getattribute__ se llama en CADA acceso a un atributo, sin excepción. Sobrescribir __getattribute__ es mucho más potente pero también mucho más peligroso: es muy fácil crear una recursión infinita si dentro de él accedes a otros atributos sin pasar por super().__getattribute__(). En la práctica, casi siempre quieres __getattr__; __getattribute__ solo cuando necesitas interceptar absolutamente todos los accesos.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Métodos especiales en Python: el protocolo dunder completo? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!