What does `__iadd__` control when Python evaluates `x += y`?
answer
- Not just shorthand for a plus
- Three steps, not one
- The method's return value is stored back
- Aliases notice what happened
- A tuple slot can still refuse the store
basics
~10 sx += y calls type(x).__iadd__(x, y) and then rebinds the name x to whatever that returns. In-place methods normally mutate the object and return self; returning None silently rebinds x to None.
solid answer
~50 sAugmented assignment is a read, operate and store back sequence, not a single atomic step. Python evaluates the target, calls the in-place method `__iadd__` if the type has one, and assigns the result back to the same target. Two consequences matter. First, the method must return the object to keep — a class that mutates `self` and forgets `return self` leaves the name bound to `None`. Second, because a well-behaved `__iadd__` mutates in place, every other name pointing at that object sees the change, which is why `a = b = []; a += [1]` also changes `b` while `a = a + [1]` does not. If the type defines no `__iadd__`, `+=` falls back to computing `x + y` and storing the new object, so `+=` still works on immutable types like `str` and `tuple` — it just rebinds.
code
python · 13 linesclass Digest:
def __init__(self, rows=()):
self.rows = list(rows)
def __iadd__(self, rows):
self.rows.extend(rows)
return self
d = Digest(["r1"])
before = id(d)
d += ["r2", "r3"]
print(d.rows, id(d) == before)go deeper
Recall that += on a list changes the list itself while += on a string or tuple makes a new object. Being able to say that much, with an example, is enough at this level.
This is your question. Explain the read-operate-store expansion, why __iadd__ must return self, and the fallback to ordinary addition when no in-place method exists. Expect to be handed the tuple-of-list puzzle.
Demonstrate the consequences in real code: shared mutable state changed through an alias, and a read-modify-write that is not atomic when two threads run it. Say what you would review for in a hand-written in-place operator.
Frame it as an API contract. Decide whether your value types are immutable, and if they are, do not offer in-place operators at all — the aliasing surprises they create cost more than the copying they save at almost every scale.
## What the statement actually does `x += y` is not sugar for `x = x + y`. It expands to three steps: evaluate the target `x`, produce a result, and store that result back into the same target. The middle step prefers the *in-place* method: Python calls `type(x).__iadd__(x, y)` when the type provides one, and only if it does not does the expression fall back to the ordinary binary-addition path used by `x + y`. Either way the store happens, and the target is rebound to whatever came out. The same shape holds for every augmented operator — `__isub__`, `__imul__`, `__itruediv__`, `__ior__`, `__iand__`, `__imatmul__` and the rest each back one `op=` statement. ## Rule one: return the object Because the result is always stored back, `__iadd__` must return the object you want the name to hold — conventionally `self`, after mutating it: ```python class Digest: def __init__(self, rows=()): self.rows = list(rows) def __iadd__(self, rows): self.rows.extend(rows) return self ``` Omit that `return self` and the method returns `None`, so `d += ["r2"]` mutates the object correctly and then binds `d` to `None`. The bug is silent at the point of the mistake and explodes far away with `AttributeError: 'NoneType' object has no attribute ...`. It is the single most common defect in a hand-written in-place operator. ## Rule two: in place means shared A correct `__iadd__` mutates the object every alias can see. That is the whole point, and it is also the trap: ```python a = b = [1] a += [2] # list.__iadd__ extends in place print(b) # [1, 2] a = b = [1] a = a + [2] # builds a new list, rebinds only a print(b) # [1] ``` So `+=` on a mutable object is a mutation with an assignment's syntax. When such an object is reachable from more than one place — a shared accumulator, a default argument, an attribute on a singleton — the operator quietly changes it for all of them. ## Rule three: the store can fail after the mutation Because the store is a separate step, it can raise *after* the in-place method has already done its work. The classic demonstration: ```pycon >>> t = ([1], 2) >>> t[0] += [3] Traceback (most recent call last): ... TypeError: 'tuple' object does not support item assignment >>> t ([1, 3], 2) ``` The list was extended by its in-place method, and only then did assigning back into the tuple slot fail. Both the exception and the mutation are correct given the three-step model, and candidates who believe `+=` is one indivisible operation cannot explain the result. That same non-atomicity is why `shared += rows` is unsafe when more than one thread touches `shared`: another thread can run between the read and the store, so an update computed from a stale value can be written back over a newer one. ## When a class should define `__iadd__` Only when in-place mutation is both meaningful and worth it. A value-like, immutable type should define `__add__` alone and let `+=` fall back to it: `str` and `tuple` do exactly that, which is why `s += "x"` rebinds `s` to a brand-new string rather than mutating one. A mutable container defines `__iadd__` when growing in place avoids copying — `list.__iadd__` behaves like `extend` and accepts any iterable, whereas `list.__add__` insists on another list and copies. If you do define it, keep the semantics honest: mutate and `return self`, keep the type stable, and do not use `+=` as a general-purpose "apply this to the object" hook. A reader expects `+=` to add something; anything else belongs in a named method. ## Answering it well Say the three steps out loud — read the target, call the in-place method, store the result — then derive the three consequences from them: `return self` is mandatory, aliases see the mutation, and the store is a separate failure point. Deriving them beats memorising them.
- What happens if `__iadd__` mutates the object but forgets to return `self`?The method returns `None`, and because augmented assignment always stores the result, the name is rebound to `None`. The object was mutated correctly, so the data is fine, but the variable is gone — the failure surfaces later as an `AttributeError` or `TypeError` on `None`, far from the operator that caused it.
- Why does `t = ([1], 2); t[0] += [3]` both extend the list and raise `TypeError`?The three steps run in order: Python reads `t[0]`, calls the list's in-place addition which extends it, then tries to store the result back into `t[0]`. Tuples reject item assignment, so the store raises — after the mutation has already happened. The list keeps the new element.
- Which types should deliberately not define `__iadd__`?Immutable value types. `str`, `tuple`, `int` and `frozenset` define only their binary operators, so `+=` falls back to building a new object and rebinding. That is what callers expect from an immutable type: no alias of the old value changes underneath them.
saying these in an interview costs you the question
- Says `x += y` is always equivalent to `x = x + y`
- Writes an `__iadd__` that mutates and returns `None`
- Thinks `+=` on a list builds a new list
- Assumes augmented assignment is one atomic step
- Believes a missing `__iadd__` makes `+=` raise `TypeError`
- Expects the tuple example to mutate nothing