In Python, why does `items += [x]` inside a function affect the caller's list but `items = items + [x]` does not?
answer
- The two forms are different operations
- One tries an in-place method first
- Then the result is assigned back
- list extends in place, tuple cannot
- `__iadd__` versus `__add__`
basics
~20 sAugmented assignment tries the in-place method first. A list has one, and it extends the existing object, so the caller sees the change. Plain + builds a new list and the assignment repoints only the local name.
solid answer
~40 s`+=` is not shorthand for `x = x + y`. It first looks for `__iadd__` on the left operand's type; `list.__iadd__` behaves like `extend`, mutating the existing list and returning that same object, and the result is then assigned back to the target — a no-op for a plain local name. So the object the caller passed really changed. `items = items + [x]` calls `__add__`, which allocates a second list, and the assignment repoints only the function's local name; the caller sees nothing. A type with no `__iadd__` — `tuple`, `int`, `str` — falls back to `__add__`, which is why `t += (3,)` inside a function is invisible. The answer therefore depends on the operand's type, not on the spelling.
code
python · 13 linesdef with_iadd(items):
items += [3]
def with_plus(items):
items = items + [3]
a = [1, 2]
with_iadd(a)
print(a) # [1, 2, 3]
b = [1, 2]
with_plus(b)
print(b) # [1, 2]go deeper
Remember that += is not always the same as x = x + y. On a list it changes the existing object, so a caller that passed that list sees the extra item; on a tuple or an int it cannot.
Explain the protocol: __iadd__ is tried first and may mutate in place and return the same object, the result is assigned back to the target, and a type without __iadd__ falls back to __add__ and builds a new object.
Show where the assign-back bites — obj.attr += [...] fires a setter and d[key] += [...] fires __setitem__ — and know that result = result + [x] in a loop is quadratic where the in-place form is not.
Treat this as a contract question, not a style one: whether a helper appends to a caller's list or hands back a new one belongs in the API's design, and hidden in-place mutation at a shared boundary is a defect worth a convention.
### `+=` is its own operation, not shorthand `items += [3]` and `items = items + [3]` look interchangeable and are not. Python compiles augmented assignment to a distinct instruction that follows the **in-place operator protocol**: 1. Look for `type(target).__iadd__`. If it exists, call it with the right operand. The method may change the object in place, and it returns the object that should now be bound to the target — conventionally `self`. 2. If `__iadd__` does not exist, fall back to `__add__` (then the right operand's `__radd__`), which builds and returns a **new** object. 3. Either way, **assign the returned object back to the target**. `list` defines `__iadd__`, and its implementation is essentially `extend`: it appends the right operand's items to the existing list and returns that same list. So inside a function, `items += [3]` mutates the one object the caller passed, and step 3 then rebinds the local name to the object it already named — a no-op. The caller sees the new element. `items = items + [3]` never touches the original. `list.__add__` allocates a second list holding the concatenation, and the plain assignment repoints only the function's local name at it. The caller's list is untouched, and the function's changes evaporate when it returns. ```python def with_iadd(items): items += [3] def with_plus(items): items = items + [3] a = [1, 2]; with_iadd(a); print(a) # [1, 2, 3] b = [1, 2]; with_plus(b); print(b) # [1, 2] ``` ### Types without an in-place add A tuple, an int, a str and a frozenset define no `__iadd__`, so `+=` on them takes the fallback path: a new object is built and the target is rebound. That is why `t += (3,)` inside a function is invisible to the caller even though the syntax is identical to the list case — the operator did not mutate anything, because on those types there is nothing to mutate. ```python def bump(t): before = id(t) t += (3,) # falls back to __add__, builds a new tuple return before == id(t) pair = (1, 2) print(bump(pair), pair) # False (1, 2) ``` The interview-grade way to state it: **`+=` mutates when the left operand's type offers an in-place add, and rebinds when it does not.** You cannot answer "does the caller see it?" from the syntax alone; you have to know the type. ### The assign-back is not always a no-op For a plain local name, step 3 is invisible. For any other kind of target it performs a real store, and that is where the surprises live. * `obj.attr += [3]` reads the attribute, extends the list in place, and then *sets* the attribute back — so a property setter, a `__slots__` descriptor or a `__setattr__` hook fires even though the list was mutated in place. * `d[key] += [3]` likewise performs a `__setitem__` after the in-place extend. * `t[0] += [3]`, where `t` is a tuple holding a list, is the classic: the in-place extend **succeeds**, then the assign-back raises `TypeError` because tuples do not support item assignment. The list really did gain the element, and the traceback claims it failed. ### Two more practical differences **Accepted operands.** `list.__iadd__` accepts any iterable, exactly like `extend`, so `items += (3, 4)` and `items += "ab"` are legal (the last one appends two one-character strings — usually a bug). `+` is stricter: `[1] + (2,)` raises `TypeError`. If you meant concatenation of two lists, `+` catches the mistake and `+=` does not. **Cost.** Building a list in a loop with `result = result + [x]` copies the whole accumulated list on every iteration, which is quadratic in the number of items. `result += [x]` — or the clearer `result.append(x)` — amortizes to constant time per item. On a large batch that difference is the whole runtime, and it is a common reason a "simple" builder loop is inexplicably slow. ### Answering it in an interview Say what the operator does before you say what the caller sees. "Augmented assignment tries the in-place method first; `list` has one and it extends the existing object, so the caller's list changes. Plain `+` has no in-place path — it builds a new list and the assignment only repoints the local name, so the caller sees nothing." Then offer the tuple case as evidence that the answer depends on the type rather than on the spelling, and mention that for a mutating helper you would write `items.append(x)` outright, because `+=` at a shared boundary hides the mutation from the reader.
- Is `items += [3]` equivalent to `items.extend([3])` from the caller's point of view?For a plain local name, yes — `list.__iadd__` is effectively `extend`, and the assign-back repoints the name at the object it already named. They differ when the target is not a plain name: `obj.attr += [3]` and `d[key] += [3]` also perform a `__setattr__` or `__setitem__` after the in-place extend, which `extend` alone would not.
- Why does `holder[0] += [2]` on a tuple holding a list both raise and succeed?The in-place extend runs first and really does mutate the list. Then the augmented assignment tries to store the result back into `holder[0]`, and tuples reject item assignment, so a `TypeError` is raised after the mutation already happened. The list keeps the new element despite the traceback.
- Does the choice matter for performance?Yes. Accumulating with `result = result + [x]` copies the whole list on every iteration and is quadratic in the number of items, while `result += [x]` or `result.append(x)` is amortized constant time per item. On a large batch that is the difference between a fast loop and an inexplicably slow one.
saying these in an interview costs you the question
- Says `+=` is always just shorthand for `x = x + y`
- Thinks `+=` on a list allocates a new list
- Assumes `+=` behaves the same for a list and a tuple parameter
- Explains it as 'lists are mutable' without naming the in-place operator
- Believes the caller sees both forms because a reference was passed