Why Python Developers Fail Default Mutable Argument Questions in Interviews

A single Python behavior has ended more technical interviews than almost any other: the default mutable argument trap. This appears in entry-level screens at startups and senior-level loops at FAANG companies alike. It is a fundamental misunderstanding of how Python handles default arguments, particularly when those defaults are mutable objects like lists or dictionaries.

The Classic Default Mutable Argument Scenario

Consider this common interview question:

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item('a'))
print(add_item('b'))
print(add_item('c'))

A developer unfamiliar with Python's nuances might expect the output to be:

['a']
['b']
['c']

However, the actual output is:

['a']
['a', 'b']
['a', 'b', 'c']

This discrepancy is where the interview question probes for understanding. The core issue lies in how Python evaluates default arguments. Unlike many other languages, Python evaluates default arguments once when the function is defined, not each time the function is called. When the default is a mutable object, this single evaluation means that the same object is reused across all calls to the function that do not explicitly provide a value for that argument.

The Pythonic Solution: Using `None` as a Sentinel

The correct and idiomatic Python way to handle functions that require mutable defaults is to use None as the default and then create a new mutable object inside the function if the argument is None.

Here's the corrected version of the function:

def add_item_pythonic(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item_pythonic('a'))
print(add_item_pythonic('b'))
print(add_item_pythonic('c'))

This version produces the expected output:

['a']
['b']
['c']

By using None as the default, we create a sentinel value. When the function is called without an explicit list, items is None. The if items is None: block then executes, creating a *new* empty list for that specific function call. Subsequent calls without an explicit list will again receive None, triggering the creation of another new list, thus isolating the state of the list for each invocation.

Why This Question is a Persistent Interview Staple

This question persists for several reasons:

  • Fundamental Language Understanding: It tests a developer's grasp of Python's execution model, scope, and object mutability. It's not just about knowing syntax; it's about understanding how Python *works* under the hood.
  • Real-World Bugs: This is not an academic curiosity. Developers who don't understand this behavior can introduce subtle, hard-to-debug errors into production code. Imagine a caching mechanism or a request processing function that inadvertently shares state across unrelated calls.
  • Problem-Solving and Debugging: Debugging issues related to shared mutable state can be incredibly difficult. A candidate who can explain this pitfall and its solution demonstrates strong debugging and analytical skills.
  • Code Maintainability: Writing code that behaves predictably, especially with default arguments, is crucial for maintainability. The None sentinel pattern leads to more robust and understandable code.

The surprise for many developers comes from the expectation that function parameters are re-evaluated on every call, similar to how they might treat local variables. However, default arguments are part of the function's definition, baked in when the interpreter first parses the code. Think of it less like a fresh variable being created each time you call the function, and more like a shortcut in a document that always points to the same, original location. If you modify what's at that location, every time you use the shortcut, you see the modified version.

Beyond Lists: Dictionaries and Other Mutables

This behavior is not limited to lists. Any mutable data type used as a default argument will exhibit the same characteristic. This includes dictionaries, sets, and custom mutable objects.

For example, consider a function to build a configuration object:

def update_config(key, value, config={{}}):
    config[key] = value
    return config

print(update_config('host', 'localhost'))
print(update_config('port', 8080))

The output will again show shared state, with the second call's dictionary containing the key-value pair from the first call. The Pythonic solution using None applies equally here:

def update_config_pythonic(key, value, config=None):
    if config is None:
        config = {{}}
    config[key] = value
    return config

This pattern is a cornerstone of writing safe and predictable Python code. Candidates who can articulate this behavior and its solution demonstrate a deep understanding of the language that goes beyond superficial syntax knowledge. It's a small detail, but one that separates proficient Python developers from those still learning the language's subtle, yet powerful, mechanics.