Defaults are evaluated once
You already know default parameters: def greet(name="friend"). Here is the detail Python for Beginners skipped, and it causes real bugs: the default value is created once, when the def line runs, not on every call.
For numbers and strings that is harmless. For a mutable value like a list or dict, it means every call that uses the default shares the same object:
def add_item(item, bucket=[]): bucket.append(item) return bucket
Call it twice without passing a bucket and the second call finds the first call's item still sitting in the list. This trap genuinely ships to production, where a shared default list quietly leaks one request's data into the next, and it is one of the most common Python interview questions. Run the next example to see it happen.
The shared list that keeps growing
Read this program and look closely at the second output line. The second call never passed a list, yet "apple" from the first call is already sitting in it.
def add_item(item, bucket=[]): bucket.append(item) return bucket print(add_item("apple")) print(add_item("banana")) print(add_item("cherry", []))
Output
['apple'] ['apple', 'banana'] ['cherry']
The empty list in the def line was created exactly once, when Python executed the def statement, and every call that omits bucket reuses that one object. The third call passes its own fresh list, which is why it escapes the accumulation and prints a single item.
The fix: a None sentinel
The standard pattern is to default to None (an immutable placeholder) and create the fresh list inside the function body, which runs on every call:
def add_item(item, bucket=None): if bucket is None: bucket = [] bucket.append(item) return bucket
Now each default call gets its own new list. Remember the rule as: never use [], {}, or any mutable object as a default value. Every experienced Python interviewer asks about this.
log_event
The fix is the None sentinel pattern. The default becomes None, an immutable value that is safe to share, and the real list gets built inside the body on every call that needs one. Now two calls produce two independent single-item lists.
def log_event(event, log=None): if log is None: log = [] log.append(event) return log print(log_event("start")) print(log_event("stop"))
Output
['start'] ['stop']
Two small changes do all the work: the default in the signature is None instead of [], and the first line of the body is if log is None: log = []. Because that assignment runs per call, each call gets its own list. Use is None rather than if not log, since an empty list a caller deliberately passed is falsy too and would be silently replaced.
Sets fall into the same trap
Writing def f(x, seen=set()) is dangerous for exactly the reason lists are. The set is built once, when the def statement runs, so every call that omits seen shares that single set and anything one call adds is still there on the next.
This is not a list-specific quirk. It applies to any object that can be changed in place, so the cure is the same: default to None and build the fresh set inside the body.
Which defaults are safe to write inline
Immutable values are safe to put directly in a def line: numbers, strings, True and False, None, and tuples.
The default object is created once, at def time, in every case. That only causes trouble when the object can be mutated, because then one call can leave changes behind for the next. Numbers, strings, booleans, None, and tuples cannot be modified in place, so sharing them across calls is harmless.
| Default | Safe inline | Why |
|---|---|---|
0, 3.14, "none", True, None | yes | immutable, cannot be changed in place |
(1, 2) | yes | tuples are immutable |
[], {}, set() | no | mutable, shared across every call |
For a list, dict, or set, default the parameter to
Noneand build the real object in the body.