2015-10-29

Python gotcha

A few weeks ago, I stumbled on a very hard to find bug while working on Kickoff Legends. To explain correctly the issue, I need to start with a simplier version.

Imagine this:

def get_status(item):
    if item is None:
       item = {}
    return item.get('status', 1)

This seems pretty simple and you won't find a bug in there. Coming from other languages though, you might have the reflex to write it like this:

def get_status(item={}):
    return item.get('status', 1)

Python veterans will understand right away. item={} is called only the first time. The dict will be assigned a memory space in the ram and it will be fixed on subsequent calls. On single threaded, single process application, you might never see a bug, but on application running concurrent requests, it becomes obvious there is a bug somewhere.

Now, let's see the actual gotcha I wanted to talk about :

class Player:
    positions = []
    current_position = None

    def move(self, position):
        self.positions.append(position)
        self.current_position = position
        return self

You will see exactly the same bug.

player1 = Player()
player1.move('go front')
player2 = Player()
player2.positions
>>> ['go front']

So in this case, the proper way to write it would be :

class Player:
    def __init__(self):
        self.positions = []
        self.current_position = None

    def move(self, position):
        self.positions.append(position)
        self.current_position = position
        return self

There you go! One easy way to test for it, is to check the memory allocation per variable. You could also use an IDE like PyCharm in debugging mode. Memory space are printed next to variables when running in debugging mode.

player1 = Player()
id(player1)
>>> 139828555449984