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.