The Pitfalls of Using Mutable Default Arguments in Python

In Python, it can be convenient to define functions with default values. These are used when a value is not explicitly provided when the function is called:

def create_new_game(player_name="Player One"):
    print(f"Welcome {player_name}!")

>>> create_new_game()
Welcome Player One!

However, unexpected behavior can arise when using mutable objects, such as lists, as default values:

def add_player(player_name, players=[]):
    players.append(player_name)
    return players

You might expect that the players list would be empty each time this function is called, but this is not the case:

>>> add_player("Ryu")
['Ryu']
>>> add_player("Chun Li")
['Ryu', 'Chun Li']
>>> add_player("Guile")
['Ryu', 'Chun Li', 'Guile']

The reason for this behavior is that default values in Python are evaluated only once, at the point of function definition, not each time the function is called. In other words, any mutable object used as the default value is created once and then reused in subsequent function calls.

On the other hand, if the default value is an immutable type, such as a number, string, or tuple, this unexpected behavior does not occur:

def add_player(player_name, players=()):
    return players + (player_name,)

>>> add_player("Ryu")
('Ryu',)
>>> add_player("Chun Li")
('Chun Li',)
>>> add_player("Guile")
('Guile',)

If you want a new list (or any mutable object) to be created each time the function is called, the best practice is to use None as the default value and then create a new object inside the function:

def add_player(player_name, players=None):
    if players is None:
        players = []
    players.append(player_name)
    return players

>>> add_player("Ryu")
['Ryu']
>>> add_player("Chun Li")
['Chun Li']
>>> add_player("Guile")
['Guile']

This behavior isn’t only applicable for lists. It applies to any mutable object used as a default value, including dictionaries and class instances.

Let’s say we have class like so:

class Player:
    def __init__(self):
        self.inventory = []

Instead of doing this:

def add_item_to_inventory_wrong(item, player=Player()):
    player.inventory.append(item)
    return player.inventory

You can do this:

def add_item_to_inventory(item, player=None):
    if player is None:
        player = Player()
    
    player.inventory.append(item)
    return player.inventory

In conclusion, understanding how Python handles default values, particularly mutable objects, can prevent unexpected bugs and help you write cleaner, more reliable code.

Always remember, if you need a fresh mutable object/list/dictionary for each function call, use None as the default value and instantiate it within the function.

Happy coding! :)

Leave a Reply

Your email address will not be published. Required fields are marked *