Understanding the python += Operator
Explains how the python += operator behaves for immutable and mutable types, where it reassigns versus mutates in place, and how to avoid common mistakes.
The python += operator performs augmented assignment: it adds a value to a variable and assigns the result back to that variable. In its simplest form, x += y is equivalent to x = x + y. That equivalence holds for immutable types, but it breaks down for mutable objects like lists. Understanding when += rebinds a name and when it mutates an object in place is the difference between code that works and subtle bugs that only appear under specific conditions.
What Augmented Assignment Actually Does
When Python evaluates x += y, it looks up the __iadd__ method on the type of x. If that method is defined, Python calls it and assigns the returned value back to x. If __iadd__ is not defined, Python falls back to x = x + y, which uses __add__.
For integers, floats, strings, and tuples, __iadd__ is not defined. These types are immutable, so x += y always creates a new object and rebinds x to it. The original object is left unchanged, and if no other references point to it, it becomes eligible for garbage collection.
a = 10 a += 5 print(a) # 15
This is straightforward. The integer 10 is discarded, and a now refers to a new integer object with value 15. No mutation occurred because integers cannot be mutated.
The Critical Difference for Lists
Lists define __iadd__, and they implement it as an in-place operation. When you write my_list += [4], Python extends the existing list object rather than creating a new one. The list's identity, as returned by id(), stays the same.
items = [1, 2, 3] print(id(items)) items += [4] print(id(items)) # same id print(items) # [1, 2, 3, 4]
This behavior is equivalent to calling items.extend([4]), not items = items + [4]. The distinction matters when other variables reference the same list.
original = [1, 2] ref = original original += [3] print(ref) # [1, 2, 3]
Because += mutated the list in place, ref sees the change. If you had written original = original + [3], a new list would have been created, and ref would still point to [1, 2].
When += Creates a New Object
For tuples, strings, and other immutable sequences, += always builds a new object. This is not just a minor implementation detail; it affects memory usage and performance when the operation is repeated in a loop.
t = (1, 2) t += (3,) print(t) # (1, 2, 3)
The tuple (1, 2) still exists in memory if another reference holds it. The variable t now points to a new tuple. For strings, repeated use of += in a loop creates a new string on every iteration, which is O(n^2) in total time for a loop of length n.
result = "" for i in range(1000): result += str(i)
Each iteration allocates a new string and copies the entire accumulated content. For small loops this is irrelevant, but for large inputs it becomes a measurable performance problem. Joining a list of parts with "".join(parts) avoids repeated allocation and is the standard remedy.
Common Mistakes with Mutable Defaults
The in-place behavior of += interacts badly with mutable default arguments. A default argument is evaluated once when the function is defined, not on every call. If you use += inside the function, you mutate the same list across all calls.
def add_item(item, container=[]): container += [item] return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
The second call sees the list mutated by the first call. This is a well-known Python pitfall. The fix is to use None as the default and create a fresh list inside the function.
def add_item(item, container=None): if container is None: container = [] container += [item] return container
Here += still mutates the list, but each call gets its own list, so the behavior is correct.
Performance and Memory Tradeoffs
When += mutates in place, it avoids allocating a new container and copying existing elements. Extending a list with += is amortized O(k), where k is the number of added elements, because the list may occasionally reallocate its internal buffer. The alternative, list = list + other, always creates a new list and copies all existing elements, making it O(n + k).
For large lists, this difference is not theoretical. Repeatedly using + on lists in a loop produces quadratic behavior because each iteration copies the entire growing list. Using += keeps the operation linear in the total number of elements added.
There is no security concern specific to +=; it does not bypass any access controls or introduce injection risks. The relevant operational concern is memory: in-place mutation means that all references to the object observe the change, which can cause surprising aliasing bugs in concurrent or callback-heavy code. If you need to avoid mutating a shared list, use new_list = old_list + [item] instead of old_list += [item].
Edge Cases and Type-Dependent Behavior
The behavior of += depends entirely on the left operand's type. For NumPy arrays, += performs in-place element-wise addition, which is efficient but also means the array is modified. For custom classes, you control the behavior by defining __iadd__. If you do not define it, Python falls back to __add__, which should return a new object.
class Counter: def __init__(self, value): self.value = value def __iadd__(self, other): self.value += other return self c = Counter(10) c += 5 print(c.value) # 15
If __iadd__ returns self, the object is mutated. If it returns a new instance, the variable is rebound. The choice should match the semantics of the class: mutable types typically mutate, immutable types return a new instance.
A common edge case is using += on a list element inside a tuple. Tuples are immutable, but the lists they contain are not.
pair = ([1], [2]) pair[0] += [3]
This raises TypeError: 'tuple' object does not support item assignment. The list is extended in place, but then Python tries to assign the result back to the tuple slot, which fails. The list inside the tuple is already modified, so the error leaves the data in a partially updated state. This is a rare but confusing situation that arises directly from the combination of in-place mutation and immutable containers.
Choosing Between += and Explicit Methods
For lists, += and extend() are functionally identical, but += reads more naturally when the right side is a literal or a short expression. Use extend() when you want to make the mutation explicit and avoid any ambiguity for readers who may not remember that += mutates lists. Use list = list + other only when you deliberately want a new list and want other references to remain untouched.
For strings, avoid += in loops that accumulate many parts. The operation itself is correct, but the repeated allocation cost is avoidable. Build a list of parts and join them once. For numeric accumulation, += is idiomatic and has no meaningful performance downside.
The decision between += and an explicit method is a maintainability choice. In-place mutation is convenient, but it creates hidden coupling between variables that share the same object. If a function accepts a list and uses +=, callers must know that the list they passed in will be modified. Using extend() makes that side effect visible at the call site, and using list = list + other avoids it entirely.