Why Mutable Default Arguments persist data across function calls.
Timothy stared at the glowing terminal, shaking his head.
"I can't trust it," he whispered. "I just can't trust the data."
Margaret looked up from her desk. She set down her pen and walked over to stand beside him. "What is it, Timothy?"
"It's this security logger," Timothy said, his voice tight with frustration. "I wrote a function to track alerts. It’s supposed to start fresh every time I call it. But it’s not. It’s remembering things it shouldn't. It’s mixing data from different shifts, and I can't find the leak."
He pointed to the code, looking defeated.
# Timothy's Function
def add_alert(message, history=[]):
history.append(message)
return history
# Call 1: Morning Shift
print("Morning:", add_alert("Server Reboot"))
# Call 2: Evening Shift
print("Evening:", add_alert("User Login"))
"Look," Timothy said, pointing at the output. "In the second call, I didn't provide a history list. So it should default to a new, empty list. But look what it printed."
Morning: ['Server Reboot']
Evening: ['Server Reboot', 'User Login']
"The 'Server Reboot' from the morning is showing up in the evening log," Timothy said, throwing his hands up. "Why is the second call stealing data from the first call?"
The Shared Object
Margaret pulled a chair over and sat next to him. "You are using a mutable object as a default argument," she said gently. "In Python, default arguments are not created fresh every time you run the function; they are created exactly once and shared across every single call."
She stood up and moved to the whiteboard. She drew a timeline.
1. Definition Time (When Python reads the code)
def add_alert(message, history=[]):
"Right here," Margaret said, pointing to the line. "When Python reads this def statement, it creates that list []. It creates it one time. It stores that specific list object in memory and attaches it to the function definition."
2. Runtime (When you call the function)
add_alert("Server Reboot")
"When you call the function," she continued, "you didn't provide a list. So Python grabs that stored list—the one it made earlier. You append 'Server Reboot' to it."
3. Runtime (The second call)
add_alert("User Login")
"Now, you call it again," Margaret said. "You didn't provide a list. So Python grabs the exact same stored list. It still has 'Server Reboot' inside it from the last time. You aren't getting a new empty list; you are getting the same list you modified five minutes ago."
Timothy stared at the board. "So the [] isn't an instruction to make a list? It is the list?"
"Exactly," Margaret said. "And because it is a list, it is mutable—it can be changed. If you change it, it stays changed."
The Runtime Fix
"So how do I make it create a new one every time?" Timothy asked.
"You have to force Python to create the list during Runtime—inside the function body," Margaret explained. "To do that, we use a default value that cannot be changed. We use None."
She erased his definition and wrote the standard fix.
# Margaret's Fix: The None Pattern
def add_alert(message, history=None):
# Check if a history list was provided
if history is None:
history = [] # Create a NEW list object right now
history.append(message)
return history
print("Morning:", add_alert("Server Reboot"))
print("Evening:", add_alert("User Login"))
Output:
Morning: ['Server Reboot']
Evening: ['User Login']
"See the difference?" Margaret asked. "Now the default value is None. None is immutable. It doesn't store data."
"When the function runs," she traced the code with her finger, "it checks: Is history None? If yes, it runs history = []. This line happens inside the function, so it runs every single time you call it. You get a brand new list object for every shift."
Timothy slumped back in his chair, the tension finally leaving his shoulders. "I thought my logic was broken. I didn't realize I was fighting the language design."
"You weren't fighting it," Margaret smiled. "You just misunderstood when the work was being done."
Margaret’s Cheat Sheet
Margaret opened her notebook to the "Functions" section.
-
The Trap:
def func(item, my_list=[]): - The Concept: Mutable Default Arguments.
- The Rule: Default argument values are evaluated only once, at Definition Time, not when the function is called.
-
The Consequence: If the default is a mutable object (like a
listordict), any changes made to it persist across all future function calls. -
The Fix: Use
Noneas the default to trigger object creation at Runtime.
def func(item, my_list=None):
if my_list is None:
my_list = []
- Why? This ensures a new object is created for every single function call.
In the next episode, Margaret and Timothy will face "Variable Scope"—where they discover how variables defined inside loops can leak into the enclosing function.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (2)
nice!
💯✨❤️