Why you should never use empty lists as default arguments.
🎧 Audio Edition: Prefer to listen? Check out the expanded AI podcast version of this deep dive on YouTube.
📺 Video Edition: Prefer to watch? Check out the 7-minute visual explainer on YouTube.
Timothy was sitting at the communal table, looking over a function he had written to track student enrollments for the chess club. Margaret noticed him tapping his pen against the table and walked over with a kind smile.
"Everything alright, Timothy?" she asked softly.
"I'm a bit confused, Margaret," Timothy replied. "I wrote a function to add students to a list. It works perfectly the first time. But the second and third times I call it, it seems to remember the students from before—even though I'm starting a brand-new list every time. It feels like the function is 'leaking' data."
Margaret pulled a rolling whiteboard over to their table. "That is a very common observation. Why don't we draw out exactly what Python is doing when you define that function?"
The Shared Memory
Timothy wrote his code on the board:
def add_student(name, student_list=[]):
student_list.append(name)
return student_list
# First call
print(add_student("Alex"))
# Second call
print(add_student("Alice"))
"I expected the second call to return just ['Alice']," Timothy explained. "But instead, it returned ['Alex', 'Alice']. It's as if student_list isn't being reset to an empty list."
Margaret picked up a blue marker and drew a box at the top of the board labeled Function Definition Time.
"Here is the key, Timothy," Margaret said kindly. "In Python, the default value for an argument is evaluated only once, at the moment the function is defined—not every time the function is called."
"Because you defined the default as a mutable list [], Python creates that list once and stores it inside the function object itself. Every time you call the function without providing a list of your own, you are pointing back to that same, original object."
Seeing the "Zombie" Variable
"So, the list stays alive as long as the function exists?" Timothy asked.
"Exactly," Margaret nodded. "We can actually watch it grow by looking at the function's internal attributes. Every function has a __defaults__ property that stores these values."
She wrote a quick demonstration:
print(f"Before: {add_student.__defaults__}")
add_student("Alex")
print(f"After: {add_student.__defaults__}")
Output:
Before: ([],)
After: (['Alex'],)
"Oh!" Timothy leaned in. "The list is literally stuck to the function. It's not a new list; it's the same object being modified over and over."
"Precisely," Margaret said. "This only happens with mutable objects like lists or dictionaries. Immutable objects like None, strings, or integers are safe because they can't be changed after they are created."
The Professional Standard
"How do I make it start fresh every time?" Timothy asked.
"The standard practice," Margaret said, "is to use None as a placeholder. Since None is immutable, it doesn't change. Then, we create the new list inside the function body."
She wiped the board and wrote the corrected version:
def add_student(name, student_list=None):
if student_list is None:
student_list = []
student_list.append(name)
return student_list
"Now," Margaret said, "the if student_list is None check runs every time the function is called. If you don't provide a list, Python creates a brand-new one. When the function finishes, that list's job is done, and the next call starts with a clean slate."
Timothy looked at the code. "It’s a small change, but the logic is completely different. One is sharing a single object; the other is creating a new one when needed."
"That's exactly it," Margaret said. "In fact, this trap is so famous that modern 'linters' like Pylint will warn you the moment they see a list used as a default. They want to make sure you're the one in control of the memory."
Margaret’s Cheat Sheet: Default Arguments
-
Safe Defaults:
None,True,False,0,""(Immutable objects). -
Dangerous Defaults:
[],{},set()(Mutable objects). - The Problem: Default arguments are evaluated once at definition time.
-
The Solution: Use
Noneas the default and initialize the mutable object inside the function body. -
The Debugger: Use
my_function.__defaults__to see if your function is secretly holding onto data.
Aaron Rose is a software engineer and technology writer at tech-reader.blog. For explainer videos and podcasts, check out Tech-Reader YouTube channel.
Top comments (2)
I ran into this exact bug while building a small API — my default list kept growing and it took hours to realize why. Using
Noneas default completely fixed it. This is one of those Python gotchas every developer learns the hard way, thanks for explaining it so clearly 👍Hi Bhavin!
I'm so glad the explanation helped! And believe me, you are in very good company. I’ve made that exact same mistake more than once myself—it's almost a "rite of passage" for Python developers.
There’s something about that simple
[]that looks so innocent until you realize it’s secretly a "Zombie Variable" living inside your function's memory! 🧟‍♂️Thanks for sharing your experience—it’s a great reminder to all of us that even a small API can be a great place to learn these fundamental nuances. Happy coding!