In Fluent Python, 2nd Editionshows a += assignment puzzle in Python 3.10, and the author use the bytecode for expression s[a] += b to explain what happens under the hood. However, in new version for Python, and in my case, it is Python 3.12, the generated bytecode is different.

Bytecode for the expression s[a] += b

>>> dis.dis('s[a] += b')
  0           0 RESUME                   0
 
  1           2 LOAD_NAME                0 (s)
              4 LOAD_NAME                1 (a)
              6 COPY                     2
              8 COPY                     2
             10 BINARY_SUBSCR
             14 LOAD_NAME                2 (b)
             16 BINARY_OP               13 (+=)
             20 SWAP                     3
             22 SWAP                     2
             24 STORE_SUBSCR
             28 RETURN_CONST             0 (None)

Explanation

Tip

The left-most column is the line number, in which RESUME, introduced in Python 3.11, is a special no-op, and it performs internal tracing, debugging and optimization checks. What we are interested is Line 1.

The second column is the offset of an instruction. Since Python 3.6, it uses 2 bytes for each instruction.

The third column is the instruction:

  • LOAD_NAME: push the value associated with the name into the stack. The forth column is the id of the name.

So, after executing LOAD_NAME twice, the stack is [s, a] (the stack top is the last).

  • COPY: copy and push the i-th item to the top of stack. (Note that the index is 1 based) This instruction is introduced in Python 3.11.

After executing COPY twice, the stack is:

gantt
    title Stack Top
    dateFormat X
    tickInterval 1day

    section  
    a   : 0, 1
    s   : 0, 1
    a  : 0, 1
	s   : 0, 1
  • BINARY_SUBSCR:get the key, and the container, respectively, and then push the value. To put it in another way, it implements:
key = STACK.pop()
container = STACK.pop()
STACK.push(container[key])

After loading b, the stack becomes:

gantt
    title Stack Top
    dateFormat X
    tickInterval 1day

    section  
    b  :  0, 1 
    s[a]   : 0, 1
    a  : 0, 1
	s   : 0, 1
  • BINARY_OP: implements the binary and in-place operators.
r = STACK.pop()
l = STACK.pop()
STACK.push(l op r)

So, the stack becomes:

gantt
    title Stack Top
    dateFormat X
    tickInterval 1day

    section  
    s[a] + b   : 0, 1
    a  : 0, 1
	s   : 0, 1
  • SWAP: swap the top of the stack with the i-th element. This instruction is introduced in Python 3.11.

After SWAP 3, the stack becomes:

gantt
    title Stack Top
    dateFormat X
    tickInterval 1day

    section   
    s   : 0, 1 
    a  : 0, 1
	s[a] + b   : 0, 1

After SWAP 2, the stack becomes:

gantt
    title Stack Top
    dateFormat X
    tickInterval 1day

    section   
    a  : 0, 1 
    s   : 0, 1 
	s[a] + b   : 0, 1

The objective of swapping twice is for the ease of getting s[a].

  • STORE_SUBSCR: to update the container value.
key = STACK.pop()
container = STACK.pop()
value = STACK.pop()
container[key] = value

After this instruction, the stack is empty, and s[a] becomes s[a] + b.

Since this assignment doesn’t return anything, it returns a constant None, which is called by RETURN_CONST, introduced in Python 3.12.