DEV Community

Cover image for Python's += Operator Is Lying to You (And Here's the Proof)
Ameer Abdullah
Ameer Abdullah

Posted on

Python's += Operator Is Lying to You (And Here's the Proof)

I want to show you something that has confused developers at every skill level, from beginners to people with years of Python experience.

Run this in your head before you read the answer:

a = [1, 2, 3]
b = a
a += [4, 5]
print(b)
Enter fullscreen mode Exit fullscreen mode

What do you think b prints?

Most people say [1, 2, 3]. The actual answer is [1, 2, 3, 4, 5].

Now run this one:

a = 10
b = a
a += 5
print(b)
Enter fullscreen mode Exit fullscreen mode

What does b print now?

The answer is 10. Unchanged.

Same operator. Completely different behavior. Here is why.


The += Operator Is Not One Thing

Most languages treat += as shorthand for x = x + something. Python does this too, but only for immutable types.

For mutable types like lists, += does something different. It calls __iadd__ which modifies the object in place rather than creating a new one.

This distinction is everything.


Tracing the List Version Step by Step

a = [1, 2, 3]
b = a
a += [4, 5]
print(b)
Enter fullscreen mode Exit fullscreen mode

Step 1: a = [1, 2, 3] creates a list object in memory. Let's call its address 1000. The name a points to address 1000.

Step 2: b = a does not copy the list. It creates another name b that also points to address 1000. Both a and b are labels on the same object.

Step 3: a += [4, 5] calls list.__iadd__([4, 5]) on the object at address 1000. This extends the list in place. The object at address 1000 is now [1, 2, 3, 4, 5]. The name a still points to address 1000.

Step 4: print(b) prints whatever is at address 1000. That is [1, 2, 3, 4, 5].

The mutation happened to the object, not the name. Both names saw the change because both names point to the same object.


Tracing the Integer Version Step by Step

a = 10
b = a
a += 5
print(b)
Enter fullscreen mode Exit fullscreen mode

Step 1: a = 10 points the name a to the integer object 10 at address 2000.

Step 2: b = a points b to the same address 2000.

Step 3: a += 5 tries to call int.__iadd__(5) but integers are immutable. They cannot be modified in place. So Python falls back to a = a + 5, which creates a brand new integer object 15 at address 3000 and rebinds the name a to 3000.

Step 4: b still points to address 2000. The integer 10 at that address was never touched. print(b) prints 10.


The Interview Trap Version

def append_to(element, target=[]):
    target += [element]
    return target

print(append_to(1))
print(append_to(2))
print(append_to(3))
Enter fullscreen mode Exit fullscreen mode

Output:

[1]
[1, 2]
[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

This combines two concepts: the mutable default argument trap and the in-place += behavior on lists. The default list persists between calls and += mutates it each time.

If you trace this correctly you need to track both facts simultaneously: the default object persists, and += modifies it in place rather than rebinding the name.


The Quick Mental Test

When you see x += something, ask one question: is x mutable or immutable?

If mutable (list, set, dict, bytearray): += modifies the existing object. All other names pointing to that object see the change.

If immutable (int, str, tuple, frozenset): += creates a new object and rebinds only the name on the left side. Other names are unaffected.

One operator, two behaviors. Once you have this mental model, it never trips you up again.

Practice predicting output on problems like this at PyCodeIt. The medium difficulty section has several variations of this pattern.


Top comments (0)