Back to Blog
Python

Understanding Python's += Operator

Explains how Python's += operator behaves for immutable and mutable types, where it reassigns versus mutates in place, and how to avoid common mistakes.

augmented assignmentpython syntaxmutable objectsin-place operationspython performance
Illustration of the python += operator showing a variable being augmented with a new value while a list object is mutated in place.

In Python, the += 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, using the normal addition protocol: __add__ first, then __radd__ if needed.

For integers, floats, strings, and tuples, __iadd__ is not defined. These types are immutable, so x += y cannot mutate an existing value; it rebinds x to a new value. No other reference to the original value sees a change.

a = 10 a += 5 print(a) # 15

This is straightforward. The variable a is rebound to an integer object with value 15. No mutation occurred because integers are immutable.

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, += rebinds the variable rather than mutating the old value. This distinction matters for memory 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 can create a new string on every iteration. The naive pattern can be quadratic in some implementations, because each concatenation may allocate a new string and copy the accumulated content. Some Python implementations optimize simple single-reference concatenations, so the exact behavior is version- and implementation-dependent.

result = "" for i in range(1000): result += str(i)

In the worst case, each iteration allocates a new string and copies the entire accumulated content. For small loops this is irrelevant, but for large inputs it can become a measurable performance problem. Joining a list of parts with "".join(parts) avoids repeated concatenation 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 amortized linear in the total number of elements added.

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 on the types of the operands, especially the left operand. 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 __iadd__, Python falls back to x = x + y, using the normal addition protocol. It is conventional for that operation to 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 relying on += in loops that accumulate many parts. The operation itself is correct, but repeated concatenation can be expensive and its exact behavior is implementation-dependent. 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.

Python's += Operator: In-Place vs Reassignment | RYUSLOG DEV