__add__ and __iadd__ define two distinct behaviors for addition in Python: one creates a new object, the other modifies the existing one. This distinction has real consequences for aliases and shared references, and it catches even experienced developers off guard.

add: the addition that creates

__add__ is called by the + operator. It must return a new object and leave the operands unchanged.

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  # new Vector(4, 6)

print(a.x, a.y)          # 1 2 — a is unchanged
print(id(a) == id(c))    # False — distinct objects

a + b calls a.__add__(b). If __add__ is not defined on a or returns NotImplemented, Python tries b.__radd__(a).

iadd: the addition that mutates

__iadd__ is called by the += operator. It modifies the object in place and returns 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  # mandatory

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 — same object

Returning self is non-negotiable: Python reassigns the result of __iadd__ to the left-hand variable. Forgetting return self is equivalent to writing a = None.

The alias trap

The copy/reference distinction becomes critical when multiple variables point to the same object.

a = Vector(1, 2)
alias = a  # alias and a point to the same object

# With __add__: a + b creates a new Vector
a = a + Vector(3, 4)
print(id(a) == id(alias))   # False — a points to a new object
print(alias.x, alias.y)     # 1 2 — alias still points to the original

# With __iadd__: a += b mutates in place
a = Vector(1, 2)
alias = a
a += Vector(3, 4)
print(id(a) == id(alias))   # True — same object
print(alias.x, alias.y)     # 4 6 — alias sees the change

When an object is shared across multiple references (class attribute, function argument, list element), += with __iadd__ propagates the change to every access point.

The silent fallback

If __iadd__ is not defined, Python uses __add__ as a fallback for +=. The operation then creates a new object instead of mutating the existing one.

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

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

a = Counter(5)
original_id = id(a)
a += Counter(3)
print(id(a) == original_id)  # False — new object, __add__ used as fallback

This behavior is correct but can be surprising: a += b acts like a = a + b rather than an in-place mutation.

Immutable types: += always copies

Immutable types (int, str, tuple) have no __iadd__. The += operator always creates a new object.

a = "hello"
original_id = id(a)
a += " world"
print(id(a) == original_id)  # False — new string

x = 10
ref = x
x += 1
print(ref)  # 10 — ref still points to the old int

Mutable types: += mutates in place

Mutable types like list have a __iadd__ that extends the list in place via extend(). The difference from + is not cosmetic.

a = [1, 2]
alias = a

a += [3]        # calls list.__iadd__, mutates in place
print(alias)    # [1, 2, 3] — alias sees the change

a = [1, 2]
alias = a
a = a + [3]     # calls list.__add__, creates a new list
print(alias)    # [1, 2] — alias still points to the old list

When to implement each

CaseRecommendation
Immutable object by design__add__ only
Mutable object with internal state__add__ + __iadd__
__iadd__ without __add__Avoid: + becomes unusable

If the object is mutable and in-place modification is semantically correct, implementing __iadd__ avoids unnecessary copies and makes the intent explicit.