__add__ e __iadd__ definen dos comportamientos distintos para la adición en Python: uno crea un nuevo objeto, el otro modifica el existente. Esta distinción tiene consecuencias reales sobre los alias y las referencias compartidas, y sorprende incluso a desarrolladores experimentados.

add: la adición que crea

__add__ es invocada por el operador +. Debe retornar un nuevo objeto y dejar los operandos sin cambios.

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

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

a = Vector(1, 2)
b = Vector(3, 4)
c = a + b  # nuevo Vector(4, 6)

print(a.x, a.y)          # 1 2 — a no cambia
print(id(a) == id(c))    # False — objetos distintos

a + b llama a a.__add__(b). Si __add__ no está definido en a o retorna NotImplemented, Python intenta b.__radd__(a).

iadd: la adición que muta

__iadd__ es invocada por el operador +=. Modifica el objeto en lugar y retorna self.

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

    def __iadd__(self, other):
        self.x += other.x
        self.y += other.y
        return self  # obligatorio

a = Vector(1, 2)
b = Vector(3, 4)
original_id = id(a)

a += b

print(a.x, a.y)                  # 4 6
print(id(a) == original_id)      # True — mismo objeto

Retornar self es no negociable: Python reasigna el resultado de __iadd__ a la variable izquierda. Olvidar el return self equivale a escribir a = None.

La trampa de los alias

La distinción copia/referencia se vuelve crítica cuando múltiples variables apuntan al mismo objeto.

a = Vector(1, 2)
alias = a  # alias y a apuntan al mismo objeto

# Con __add__: a + b crea un nuevo Vector
a = a + Vector(3, 4)
print(id(a) == id(alias))   # False — a apunta a un nuevo objeto
print(alias.x, alias.y)     # 1 2 — alias sigue apuntando al original

# Con __iadd__: a += b muta en lugar
a = Vector(1, 2)
alias = a
a += Vector(3, 4)
print(id(a) == id(alias))   # True — mismo objeto
print(alias.x, alias.y)     # 4 6 — alias ve el cambio

Cuando un objeto se comparte entre múltiples referencias (atributo de clase, argumento de función, elemento en una lista), += con __iadd__ propaga la modificación a todos los puntos de acceso.

El fallback silencioso

Si __iadd__ no está definido, Python usa __add__ como fallback para +=. La operación crea entonces un nuevo objeto en lugar de mutar el existente.

class Counter:
    def __init__(self, n):
        self.n = n

    def __add__(self, other):
        return Counter(self.n + other.n)
    # sin __iadd__

a = Counter(5)
original_id = id(a)
a += Counter(3)
print(id(a) == original_id)  # False — nuevo objeto, __add__ usado como fallback

Este comportamiento es correcto pero puede sorprender: a += b actúa como a = a + b en lugar de como una mutación en lugar.

Tipos inmutables: += siempre por copia

Los tipos inmutables (int, str, tuple) no tienen __iadd__. El operador += siempre crea un nuevo objeto.

a = "hello"
original_id = id(a)
a += " world"
print(id(a) == original_id)  # False — nueva cadena

x = 10
ref = x
x += 1
print(ref)  # 10 — ref sigue apuntando al int original

Tipos mutables: += muta en lugar

Los tipos mutables como list tienen un __iadd__ que extiende la lista en lugar mediante extend(). La diferencia con + no es cosmética.

a = [1, 2]
alias = a

a += [3]        # llama a list.__iadd__, muta en lugar
print(alias)    # [1, 2, 3] — alias ve el cambio

a = [1, 2]
alias = a
a = a + [3]     # llama a list.__add__, crea una nueva lista
print(alias)    # [1, 2] — alias sigue apuntando a la lista antigua

Cuándo implementar cada uno

CasoRecomendación
Objeto inmutable por diseñoSolo __add__
Objeto mutable con estado interno__add__ + __iadd__
__iadd__ sin __add__Desaconsejado: + queda inutilizable

Si el objeto es mutable y la modificación en lugar es semánticamente correcta, implementar __iadd__ evita copias innecesarias y hace explícita la intención.