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 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:
EventLogger("auth", []).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 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.
[], {}, 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.
