Python's Mutable Default Argument Trap

2026-04-24

You're building a simple event logging system. Each logger instance should maintain its own list of events. A colleague writes this clean, Pythonic class:

class EventLogger:
    def __init__(self, name, events=[]):
        self.name = name
        self.events = events

    def log(self, event):
        self.events.append(event)
        return f"[{self.name}] Logged: {event}"

    def get_events(self):
        return list(self.events)


# Create separate loggers for two subsystems
auth_logger = EventLogger("auth")
payment_logger = EventLogger("payment")

auth_logger.log("user_login")
auth_logger.log("token_issued")
payment_logger.log("charge_created")

print(f"Auth events: {auth_logger.get_events()}")
print(f"Payment events: {payment_logger.get_events()}")

Expected output:

Auth events: ['user_login', 'token_issued']
Payment events: ['charge_created']

Actual output:

Auth events: ['user_login', 'token_issued', 'charge_created']
Payment events: ['user_login', 'token_issued', 'charge_created']

Every logger shares the same events. In production, this means your auth logs contain payment data, your payment logs contain auth data, and your compliance team is having a very bad day.

The Bug

The culprit is events=[] in the function signature. In Python, default argument values are evaluated once — at function definition time, not at each call. That empty list [] is created a single time when the class is loaded, and every call to __init__ that doesn't pass an explicit events argument receives a reference to that same list object.

You can verify this directly:

print(auth_logger.events is payment_logger.events)  # True

They're not just equal — they're the same object in memory. Every .append() mutates the single shared list.

This is especially insidious because:

The same trap applies to any mutable default: dictionaries (cache={}), sets (seen=set()), or even nested lists. Immutable defaults like None, 0, True, and tuples are safe because they can't be mutated in place.

The Fix

The standard Python idiom is to use None as a sentinel and create the mutable object inside the function body:

class EventLogger:
    def __init__(self, name, events=None):
        self.name = name
        self.events = events if events is not None else []

    def log(self, event):
        self.events.append(event)
        return f"[{self.name}] Logged: {event}"

    def get_events(self):
        return list(self.events)

Now each instance gets its own fresh list. The if events is not None check (using is, not ==) also lets callers pass in an existing list when they genuinely want shared state.

Linters like pylint (W0102: dangerous-default-value) and ruff (B006) will flag this automatically. If you're not running one of these, you should be — this bug is too common and too silent to catch by eye.

Key Takeaway: Never use a mutable object ([], {}, set()) as a default argument in Python — use None and create the object inside the function body, because defaults are evaluated once at definition time and shared across all calls.

All newsletters