Back to Blog
Python

Python __iadd__: How += Mutates or Reassigns

python **iadd**: Explains Python's __iadd__ method, how += dispatches to it, the fallback to __add__, and why mutable and immutable types behave differently.

dunder methodsoperator overloadingin-place operationsmutable objectsPython internals
Diagram showing how Python's += operator dispatches to __iadd__ for in-place mutation versus creating a new object.

In Python, the += operator is syntactic sugar for a method call. When you write a += b, the interpreter first looks for a.__iadd__(b). The i stands for "in-place," and the method's job is to add b to a and return the result, which is then assigned back to the name a. Understanding python **iadd** is the key to predicting whether an augmented assignment mutates an existing object or silently creates a new one.

What __iadd__ Does and When Python Calls It

The critical detail is that __iadd__ is expected to mutate the receiver and return it. Unlike __add__, which builds a brand-new object, __iadd__ is allowed to change the existing object in place and return self. That is why list.__iadd__ behaves like extend rather than like + on tuples.

class Counter: def __init__(self, value=0): self.value = value def __iadd__(self, other): self.value += other return self c = Counter(5) c += 3 print(c.value) # 8

Here c += 3 calls c.__iadd__(3), which increments the internal value attribute and returns self. The name c still references the same object; only its state changed.

The Fallback Path: When __iadd__ Is Not Defined

If a type does not define __iadd__, Python falls back to the regular addition path. The expression a += b is then evaluated as a = a.__add__(b), or a = b.__radd__(a) if a does not implement __add__ either.

This fallback is not a subtle edge case; it is the normal behavior for immutable types. Integers, strings, and tuples do not implement __iadd__ because they cannot be mutated. When you write:

x = 10 x += 5

Python evaluates x = x.__add__(5), creates a new integer object with value 15, and rebinds x to it. The original integer 10 is unchanged and becomes eligible for garbage collection.

The same fallback applies to user-defined classes that only implement __add__. If you forget to define __iadd__, += still works, but it produces a new object instead of mutating the existing one. That difference matters when the object is referenced from multiple places.

Mutable vs. Immutable Behavior: Why Lists and Tuples Differ

The distinction between __iadd__ and the fallback to __add__ is easiest to see by comparing lists and tuples.

items = [1, 2] original = items items += [3] print(items) # [1, 2, 3] print(original) # [1, 2, 3] -- same object

Because list defines __iadd__, the operation mutates the existing list. The variable original still points to the same list object, so it sees the change.

t = (1, 2) original = t t += (3,) print(t) # (1, 2, 3) print(original) # (1, 2) -- new object

Tuples have no __iadd__, so += falls back to __add__ and creates a new tuple. The name t is rebound, but original still references the old tuple. This is why aliasing bugs appear with mutable types: any code holding a reference to the list observes the mutation, while code holding a reference to the tuple does not.

Implementing __iadd__ on a Custom Class

When designing a mutable container or accumulator, implementing __iadd__ gives callers the same in-place semantics they expect from built-in lists. A common pattern is a class that aggregates items:

class Bundle: def __init__(self, items=None): self.items = list(items) if items else [] def __iadd__(self, other): self.items.extend(other) return self def __add__(self, other): result = Bundle(self.items) result.items.extend(other) return result

With both methods defined, bundle += extra mutates the existing bundle, while bundle + extra returns a new bundle and leaves the original untouched. This matches the behavior of list and gives users a predictable API.

Note that __iadd__ and __add__ do not have to share an implementation. __iadd__ can be more efficient because it avoids copying the existing contents. In the example above, __add__ copies the list, while __iadd__ does not.

The return self Requirement and What Breaks Without It

The most common bug when implementing __iadd__ is forgetting to return anything. The augmented assignment a += b always assigns the return value of the method back to the left-hand name. If __iadd__ returns None, the name becomes None.

class BrokenCounter: def __init__(self, value=0): self.value = value def __iadd__(self, other): self.value += other # missing: return self c = BrokenCounter(5) c += 3 print(c) # None

The mutation did happen; the internal value was incremented. But because the method returned None, the assignment c = c.__iadd__(3) rebinds c to None, and the original object is lost. This is a silent failure that often surfaces later as an AttributeError or a confusing None value in a data structure.

The same rule applies to __imul__, __isub__, and the other in-place operators. They must all return the object that should be bound to the left-hand name, which is normally self.

Memory and Allocation Implications of In-Place Addition

The practical reason to implement __iadd__ is to avoid allocating a new object on every operation. For a loop that accumulates values, the difference can be significant.

def accumulate_with_plus(n): total = [] for i in range(n): total = total + [i] # new list each iteration return total def accumulate_with_iadd(n): total = [] for i in range(n): total += [i] # extends in place return total

The first version creates a new list on every iteration and copies all previous elements into it, giving O(n²) copying work overall. The second version calls list.__iadd__, which extends the existing list in place and typically amortizes to O(n) total work. The exact allocation behavior depends on the list's internal capacity growth, but the structural difference is clear: one path copies the entire prefix each time, and the other does not.

For immutable types there is no choice; += must allocate because the original cannot change. For custom mutable classes, implementing __iadd__ is the way to give users the efficient path.

Interaction with __add__ and __radd__ in Subclasses

When a class inherits __iadd__ from a parent, subclass instances use the parent's implementation unless the subclass overrides it. A subtle issue arises when __iadd__ operates on a property that the subclass changes.

class BaseList(Bundle): pass class TaggedList(BaseList): def __init__(self, items=None, tag=""): super().__init__(items) self.tag = tag

Here TaggedList inherits __iadd__ from Bundle. Since Bundle.__iadd__ returns self, the tag attribute is preserved, and the operation mutates the existing instance. If the parent's __iadd__ had instead returned a new object of the parent type, the subclass's extra attributes would be lost. This is why returning self rather than constructing a new instance is not just a convention; it preserves the dynamic type and all subclass state.

The __radd__ method is only relevant when the left operand does not support the operation. For a += b, Python tries a.__iadd__(b) first, then a.__add__(b), then b.__radd__(a). In practice, __radd__ matters more for + expressions where the left operand is a built-in type, such as int + MyClass. For +=, the left operand is usually the object being mutated, so __radd__ rarely comes into play.

python **iadd**: Practical Usage and Code Examples | RYUSLOG DEV