July 18, 2015 · python

Always use None for default args in Python

I've came across plenty of Python developers who use default values directly in their method signature - after all, why wouldn't they? At first, that seemed a completely logical thing to do, but after working with Python for years I've come to realize that is almost never what you want - and here's why:

The Problem

Using anything but None for default arguments makes objects and functions less extensible.

Consider the following example:

class Cat:
   def eat(self, food='catfood'):
       return food

cat = Cat()
cat.eat()  # catfood
cat.eat('steak')  # steak

Here we have a Cat with an eat method that has the argument food='catfood'.

Later, someone comes along and creates a more specialized Cat; a Tabby. Our new class still eats catfood, and the developer is forced to duplicate the default argument:

from farm.cats import Cat

class Tabby(Cat):
    def eat(self, food='catfood'):
        # do something else
        return super().eat(food)

We can make this a bit better by using a constant to store our default value instead:

DEFAULT_FOOD = 'catfood'

class Cat:
   def eat(self, food=DEFAULT_FOOD):
       return food

But now our overriding class has to import the constant to maintain the method signature:

from farm.cats import Cat, DEFAULT_FOOD

class Tabby(Cat):
    def eat(self, food=DEFAULT_FOOD):
        # do something
        return super().eat(food)

This is more involved than it should be.

The Solution

To get around this problem, and generally design more extensible classes, we're going to use None for our default argument, subbing in our default value when none is provided:

DEFAULT_FOOD = 'catfood'

class Cat:
    def eat(self, food=None):
        if food is None:
            food = DEFAULT_FOOD
        return food

cat = Cat()
cat.eat()  # catfood
cat.eat('tuna')  # tuna

Our class still behaves the same, but is easier to extend. Look how we no longer need to worry about the default value constant in our Tabby to maintain the same method signature:

from farm.cats import Cat

class Tabby(Cat):
    def eat(self, food=None):
        # do something
        return super().eat(food)

tabby = Tabby()
tabby.eat()  # catfood
tabby.eat('fancyfeast')  # fancyfeast

We can improve on this still by favoring composition:

class Tabby:
    def __init__(self, cat):
        self.cat = cat

    def eat(self, food=None):
        # do something
        return self.cat.eat(food)

Now our Tabby doesn't need to import anything, and can work on any object that acts like a Cat.

In closing

Don't use actual values for default arguments unless you have a very compelling reason; they make your classes much less extensible. Do use composition where practical, it results in looser coupling.

If that wasn't convincing enough, consider that default arguments are mutable:

def fn(list=[]):
    list.append(100)
    return list

fn()  # [100]
fn()  # [100, 100]
fn()  # [100, 100, 100]

Yikes.