Navigation

Python

How to Handle Mutable Default Arguments

Avoid Python's mutable default argument trap. Use None as default and create new objects inside functions to prevent shared state bugs

Table Of Contents

Problem

Using mutable objects like lists or dictionaries as default arguments creates shared state between function calls, causing unexpected behavior.

Solution

# Wrong: Mutable default argument (creates shared state)
def bad_append(item, target_list=[]):
    target_list.append(item)
    return target_list

print(bad_append(1))      # [1]
print(bad_append(2))      # [1, 2] - Unexpected!
print(bad_append(3))      # [1, 2, 3] - Still growing!

# Correct: Use None and create new object inside function
def good_append(item, target_list=None):
    if target_list is None:
        target_list = []
    target_list.append(item)
    return target_list

print(good_append(1))     # [1]
print(good_append(2))     # [2] - Correct!
print(good_append(3))     # [3] - Correct!

# Dictionary example - Wrong way
def bad_add_config(key, value, config={}):
    config[key] = value
    return config

print(bad_add_config('name', 'John'))        # {'name': 'John'}
print(bad_add_config('age', 25))             # {'name': 'John', 'age': 25} - Shared!

# Dictionary example - Correct way
def good_add_config(key, value, config=None):
    if config is None:
        config = {}
    config[key] = value
    return config

print(good_add_config('name', 'John'))       # {'name': 'John'}
print(good_add_config('age', 25))            # {'age': 25} - Separate!

# Alternative pattern with copy
def append_with_copy(item, target_list=None):
    if target_list is None:
        target_list = []
    else:
        target_list = target_list.copy()  # Create copy to avoid modifying original
    target_list.append(item)
    return target_list

original = [1, 2]
result = append_with_copy(3, original)
print(original)  # [1, 2] - Unchanged
print(result)    # [1, 2, 3] - New list

# Class with mutable default
class UserManager:
    def __init__(self, users=None):
        self.users = users if users is not None else []
    
    def add_user(self, name):
        self.users.append(name)

manager1 = UserManager()
manager1.add_user('Alice')

manager2 = UserManager()
manager2.add_user('Bob')

print(manager1.users)  # ['Alice'] - Correct
print(manager2.users)  # ['Bob'] - Correct

# Debugging mutable defaults
def debug_function(items=[]):
    print(f"Default object ID: {id(items)}")
    items.append(len(items))
    return items

result1 = debug_function()  # ID: 140234567890
result2 = debug_function()  # ID: 140234567890 - Same object!

# Safe with immutable defaults
def safe_function(message="Hello"):
    return message.upper()

print(safe_function())        # HELLO
print(safe_function("Hi"))    # HI

# Factory function pattern
def create_list():
    return []

def safe_append(item, target_list=None):
    if target_list is None:
        target_list = create_list()
    target_list.append(item)
    return target_list

Explanation

Python evaluates default arguments once when the function is defined, not each time it's called. Mutable objects like lists and dicts are shared across calls.

Always use None as default and create new objects inside the function. This ensures each function call gets a fresh, independent object.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Python