python multiple-assignment pitfall

Fell into a pit while trying to reverse a linked list!

Here’s the typical definition of a Node in a linked list

class Node:
    def __init__(self, val, next):
        self.val = val
        self.next = next

# To save typing:
N = Node

I set out to reverse a linked list with this template, because I wanted to start off by making sure that the loop advances.

def reverse(head):
    rev = None
    p = head
    while p is not None:

        ...

        p = p.next

Continuing from there, I would then actually do the reversal using python’s multiple assignments

def reverse(head):
    rev = None
    p = head
    while p is not None:
        p, p.next, rev = p.next, rev, p

    return rev

I ran it on my little test case

def test_reverse():
    head = N(1, N(2, N(3, None)))
    rev = reverse(head)
    assert rev.val == 3
    assert rev.next.val == 2
    assert rev.next.next.val == 1
    assert rev.next.next.next is None

And promptly got a AttributeError: 'NoneType' object has no attribute 'next'!

It turns out that when using python’s multiple assignments, the tuple on the right of the assignment is evaluated completely, but there is still an order to the assignments on the left, and the assignments go from left to right.

This means, on the third iteration of the loop above, p is assigned None, then p.next is evaluated to try and assign rev to the next attribute of None, which fails.

The fix is simply to swap the order around so that the assignment of p.next to p goes last:

def reverse(head):
    rev = None
    p = head
    while p is not None:
        rev, p.next, p = p, rev, p.next

    return rev

Here are the details from Python’s bytecode disassembly:

$ python
Python 3.8.2 (default, Feb 29 2020, 11:29:25)
[GCC 9.2.1 20200130] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> dis.dis("rev, p.next, p = p, rev, p.next")
  1           0 LOAD_NAME                0 (p)
              2 LOAD_NAME                1 (rev)
              4 LOAD_NAME                0 (p)
              6 LOAD_ATTR                2 (next)
              8 ROT_THREE
             10 ROT_TWO
             12 STORE_NAME               1 (rev)
             14 LOAD_NAME                0 (p)
             16 STORE_ATTR               2 (next)
             18 STORE_NAME               0 (p)
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE
>>>

Here’s what the columns in that output mean

Here’s a visualization of the progress

|       after |              |                        |
| instruction |              |                        |
|      offset | stack        | notes                  |
| ----------- | ------------ | ---------------------- |
|           0 | p            |                        |
|           2 | p, rev       |                        |
|           6 | p, rev, next |                        |
|           8 | next, p, rev |                        |
|          10 | next, rev, p |                        |
|          12 | next, rev    | p stored into rev      |
|          16 | next         | rev stored into p.next |
|          18 |              | next stored into p     |
comments powered by Disqus