Skip to content

+= is not +

Level: 201 · for Python programmers

One line: a += b first asks the object to change itself, and only when it cannot — a tuple, a string, an int — falls back to a = a + b; so on a list every other name for that list sees the change, and t[0] += [1] on a tuple both raises and happens.

a = [1]
b = a
a += [2]
b           # [1, 2]
a = a + [3]
b           # [1, 2]

+= is not an abbreviation of + and =. It is its own operation with its own method: x += y calls type(x).__iadd__(x, y) if that method exists and binds x to whatever it returns, and only if there is no __iadd__ does it compute x + y with __add__ and bind x to the result. A list has __iadd__, which extends the list in place and returns the same object, so the binding changes nothing and every other name for the list sees the new elements. A tuple has no __iadd__, so t += (2,) builds a new tuple and points t at it, and every other name keeps the old one. One spelling, two behaviours, chosen by the type on the left.

That fork has two consequences worth a page. The first is that += on a list is a mutation that every alias observes, as Assignment does not copy says any mutation is. The second is stranger: an augmented assignment to an element of a tuple can raise an exception after it has already done its work.

The run

Verified output of plus_equals_is_not_plus_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. ON A LIST, += CHANGES THE OBJECT
     a = [1]
     b = a
     a += [2]
     b                                [1, 2]           b saw it: += grew the one list in place
     a is b                           True
     a = a + [3]
     b                                [1, 2]           + built a new list and bound a to it; b kept the old one
     a is b                           False

2. ON A TUPLE, A STRING OR AN INT, += REBINDS
     t = (1,)
     u = t
     t += (2,)
     u                                (1,)             a tuple cannot change, so t was rebound
     t is u                           False
     s = 'ab'
     r = s
     s += 'c'
     r                                'ab'
     n = 1
     m = n
     n += 1
     m                                1

3. THE METHOD THAT DECIDES
     hasattr(list, '__iadd__')        True             list can change itself, so += calls this
     hasattr(tuple, '__iadd__')       False            no __iadd__: += falls back to __add__, then assigns
     hasattr(str, '__iadd__')         False
     hasattr(int, '__iadd__')         False
     hasattr(bytearray, '__iadd__')   True             the same split as list against tuple
     hasattr(bytes, '__iadd__')       False
     a = [1]
     a.__iadd__([2]) is a             True             the in-place method returns the object it changed

4. THE TUPLE TRAP: IT RAISES, AND IT ALSO HAPPENED
     t = ([], 'x')
     t[0] += [1]                      TypeError
     t                                ([1], 'x')       the list grew, then the store back into the tuple failed
     t2 = ([], 'x')
     t2[0] = t2[0] + [1]              TypeError
     t2                               ([], 'x')        with +, nothing had happened when the store failed
     t3 = ([], 'x')
     t3[0].extend([1])
     t3                               ([1], 'x')       no store into the tuple, so no error

5. THROUGH AN INSTANCE, += ON A CLASS ATTRIBUTE
     class Counter: count = 0
     c = Counter()
     c.count += 1
     Counter.count                    0                the read found the class attribute
     vars(c)                          {'count': 1}     the store made an instance attribute
     class Log: lines = []
     lg = Log()
     lg.lines += ['a']
     Log.lines                        ['a']            a list: the read found the class's list and += grew it
     vars(lg)                         {'lines': ['a']} and the store still made an instance attribute
     lg.lines is Log.lines            True             which names the same, now shared, list

6. THE SAME SPLIT FOR *=
     a = [0]
     b = a
     a *= 2
     b                                [0, 0]           in place
     s = 'ab'
     r = s
     s *= 2
     r                                'ab'             rebound

What the run shows

Section 1: on a list, += changes the object. a += [2] grows the one list, so b, which names the same list, shows [1, 2], and a is b stays True. a = a + [3] builds a new list and binds a to it; b keeps [1, 2] and a is b is False. Whether a second name notices depends on which of the two statements you wrote.

Section 2: on a tuple, a string or an int, += rebinds. None of the three can change, so t += (2,), s += 'c' and n += 1 each build a new object and rebind the name on the left. The other name keeps the old object in every case, and t is u is False. On these types += really is a = a + b.

Section 3: the method that decides. hasattr(list, '__iadd__') is True and hasattr(tuple, '__iadd__') is False, and that pair of facts is the whole rule: with the method, += changes the object; without it, += falls back to __add__ and assigns. str and int are on the tuple's side. bytearray is on the list's side and bytes is not, which is the split the bytearray page measures from the other direction. a.__iadd__([2]) is a is True: the in-place method returns the very object it changed, and that return value is what += then binds a to.

Section 4: the tuple trap. t[0] += [1] is three steps: read t[0], call its __iadd__, and store the result back into t[0]. The first two succeed, so the list grows. The third is an assignment into a tuple, which raises TypeError. The exception is real and so is the change: t is ([1], 'x') afterwards. t2[0] = t2[0] + [1] raises the same exception with nothing changed, because + built a new list rather than touching the old one, and the failing store was the first thing that touched t2. t3[0].extend([1]) does the same work as the first statement with no store into the tuple at all, so it raises nothing.

Section 5: through an instance, += on a class attribute. c.count += 1 reads count, finds it on the class, adds one, and stores the result on the instance, because a store through c. always writes to c's own namespace. The class still says 0; vars(c) says 1. With the list the read finds the class's list and += grows it in place, so Log.lines changes for every instance, and then the store still makes an instance attribute, which names that same list. Both halves of this section are A class attribute is shared seen through one operator.

Section 6: the same split for *=. Every augmented operator has an in-place method, __imul__, __ior__ and so on, and each falls back the same way. a *= 2 doubles the one list in place; s *= 2 rebinds.

If you are coming from C

a += b in C is a = a + b with the left side evaluated once, for every type that supports it, and no type can redefine it. There is no version that mutates a shared object, because there is no shared object: an array cannot be assigned to at all, and p += 1 on a pointer moves the pointer rather than changing what it points at. Python's += is closer to a method call than to C's operator, and section 3 is how to see which method.

If you are coming from Rust

Rust makes the two operations two traits, Add and AddAssign, and a type implements each separately. String implements AddAssign<&str> and pushes in place; Vec implements neither for another Vec, and you write extend. That is the same design as Python's __add__ and __iadd__ pair, with one difference that removes this page's trap: a String has exactly one owner, so no other name can observe that it changed in place. The Rust library's Operators are traits ↗ is the mechanism from the Rust side.

If you are coming from ABAP

ABAP's calculation assignments, lv_a += 1 and the rest, exist since release 7.54 and work on elementary values: += computes and assigns, and nothing in the language lets a type intercept it. Internal tables are extended with APPEND LINES OF or INSERT LINES OF, and because ABAP assigns by value, no second variable can observe the change either way. So both of this page's surprises are absent in ABAP, and they arrive together with Python's reference semantics rather than with its operator. (Not machine-checked — CI cannot run ABAP.)

Try it

  1. Search a codebase you maintain for += on a name that is also a default argument, a module-level constant or a class attribute: any of the three is a list other code shares, and section 1 says the += changed it for them.
  2. In a class of yours with a list or dict at class level, do instance.attr += [...] and then print vars(instance) and the class attribute. Section 5 says you will find both changed.
  3. Replace an x = x + [item] inside a loop with x += [item], then look for every other name for x: a variable it was assigned from, a parameter it came in as. Each one now sees the loop's work.
  4. Run d = {'k': (1,)} and then d['k'] += (2,). Predict whether it raises before you run it, and then explain why d['k'] differs from t[0] in section 4.
  5. Take a function that returns a list it also keeps, such as a cache. Call it and += onto the result. Then call it again.

See also