Why Does My Python Function Remember Previous Calls?
You call a function twice, expecting each call to start with an empty list. Instead, the second result contains data from the first call.
The cause may be a mutable default argument: a list created when the function is defined and reused across calls.
Let’s see how this connects to references and copying.
1. The list that keeps growing
Consider a function that records completed lessons:
def complete_lesson(lesson, completed=[]):
completed.append(lesson)
return completed
print(complete_lesson("Variables"))
print(complete_lesson("Loops"))
Output:
['Variables']
['Variables', 'Loops']
Why does the second call include "Variables"?
Python evaluates default argument expressions when it executes the function definition. It does not create a new default list each time you call the function.
Both calls use and modify the same list.
2. Returned results can share that list too
The sharing becomes clearer when we keep both results:
def complete_lesson(lesson, completed=[]):
completed.append(lesson)
return completed
first = complete_lesson("Variables")
second = complete_lesson("Loops")
print(first)
print(second)
print(first is second)
Output:
['Variables', 'Loops']
['Variables', 'Loops']
True
The function returns its list without copying it.
Consequently, first and second refer to the same object. The second call changes what you see through first too.
3. Create a fresh list inside the function
If each call should start independently, use None as the default:
def complete_lesson(lesson, completed=None):
if completed is None:
completed = []
completed.append(lesson)
return completed
first = complete_lesson("Variables")
second = complete_lesson("Loops")
print(first)
print(second)
print(first is second)
Output:
['Variables']
['Loops']
False
The list expression [] now runs inside the function whenever the argument is omitted or explicitly set to None.
Each of these calls creates a separate list.
4. An explicitly supplied list can still change
The None pattern prevents accidental sharing of a default list. It does not prevent the function from modifying a list supplied by the caller.
def complete_lesson(lesson, completed=None):
if completed is None:
completed = []
completed.append(lesson)
return completed
my_lessons = ["Variables"]
result = complete_lesson("Loops", my_lessons)
print(my_lessons)
print(result is my_lessons)
Output:
['Variables', 'Loops']
True
The parameter completed refers to the caller’s list. Calling .append() modifies that object.
This behaviour can be useful, but it should be intentional.
5. Copy the input when you want a new result
If the function should preserve the supplied list, copy it before making changes:
def complete_lesson(lesson, completed=None):
if completed is None:
updated = []
else:
updated = completed.copy()
updated.append(lesson)
return updated
my_lessons = ["Variables"]
result = complete_lesson("Loops", my_lessons)
print(my_lessons)
print(result)
print(result is my_lessons)
Output:
['Variables']
['Variables', 'Loops']
False
For this list of strings, a shallow copy provides the separation we need.
For nested mutable data, you would also need to consider whether the inner objects should remain shared.
Three questions to ask when reading a function
When a function accepts or returns a list, check:
- When is the list created?
- Does the function modify an existing list?
- Does the returned value share an object with another variable?
These questions help explain both mutable default arguments and unexpected changes to function inputs.
For further reading, see the official Python tutorial on default argument values.
Free PCEP practice test
Preparing for PCEP? Access my AM Academy free practice test here: https://www.udemy.com/course/python-pcep-30-03-certification-practice-tests-2026/?couponCode=FREE-COURSE-PCEP. I’m the creator of this independent preparation resource; it is not affiliated with or endorsed by the Python Institute.
Top comments (0)