The Mutable Default
You are reviewing a class for a shopping cart:
class Cart:
def __init__(self, items=[]):
self.items = items
def add(self, item):
self.items.append(item)
def __str__(self):
return f"Cart({self.items})"
A test script runs:
c1 = Cart()
c1.add("apple")
c2 = Cart()
c2.add("banana")
print(c1)
print(c2)
The output is:
Cart(['apple', 'banana'])
Cart(['apple', 'banana'])
The developer is surprised that both carts contain the same items. They argue that each Cart() call should create a new empty list. They also note that if they change __init__ to def __init__(self, items=None): and then set self.items = items if items is not None else [], the problem goes away.
1. Explain why the original code causes both `Cart` instances to share the same list. What is the underlying Python behavior?
Read the worked answer
In Python, default argument values are evaluated once, at the time the function is defined, not each time the function is called. So the expression [] in def __init__(self, items=[]): creates a single list object when the class is defined. That same list object is used as the default for every call to __init__ that does not provide an items argument. Thus, c1 = Cart() and c2 = Cart() both assign the same list object to self.items. When c1.add("apple") appends to that list, and then c2.add("banana") appends to the same list, both carts see the combined list. This is a common pitfall with mutable default arguments.
2. The developer's proposed fix uses `items=None` and then assigns `self.items = items if items is not None else []`. Does this fully solve the problem? Are there any remaining issues or edge cases?
Read the worked answer
The fix solves the shared mutable default problem because each call to __init__ without an items argument now creates a new empty list. However, there is a subtle issue: if a caller passes a list explicitly, the cart will store a reference to that list, not a copy. So if the caller later modifies the original list, the cart's items will change unexpectedly. For example:
my_list = ["milk"]
c = Cart(my_list)
my_list.append("eggs")
print(c) # Cart(['milk', 'eggs'])
This may or may not be desired. To make the cart independent of the input list, you could copy it: self.items = list(items) if items is not None else []. Additionally, if the caller passes a non-list iterable (e.g., a tuple), the current code would store that iterable, and append would fail. So the fix is correct for the mutable default issue but may need further refinement depending on requirements.