Do Not Use Mutable Default Arguments#

Twitter Handle LinkedIn Profile GitHub Profile Tag

from typing import List, TypeVar

T = TypeVar("T")


def append_to_list_mutable_default(item, target_list: List[T] = []) -> List[T]:
    target_list.append(item)
    return target_list
print(append_to_list_mutable_default(item=1))  # Output: [1]
print(append_to_list_mutable_default(item=2))  # Output: [1, 2]
print(append_to_list_mutable_default(item=3, target_list=[]))  # Output: [3]
print(append_to_list_mutable_default(item=4))  # Output: [1, 2, 4]
[1]
[1, 2]
[3]
[1, 2, 4]

Potential Pitfalls/Inefficiencies:

  • Mutable Default Argument: The default list target_list is mutable and persists across function calls, leading to unexpected behavior.

Questions:

  1. What will be the output of the provided example usage? Explain why.

  2. What is the issue with using mutable default arguments in Python functions?

  3. How would you modify the append_to_list function to avoid this pitfall?

1. What will be the output of the provided example usage? Explain why.#

  1. First Call: append_to_list_mutable_default(1)

    • Behavior: Since no target_list is provided, the default list [] is used.

    • Action: Appends 1 to the default list.

    • Result: The default list becomes [1].

    • Output: [1]

  2. Second Call: append_to_list_mutable_default(2)

    • Behavior: Again, no target_list is provided, so the same default list (which now contains [1]) is used.

    • Action: Appends 2 to the default list.

    • Result: The default list becomes [1, 2].

    • Output: [1, 2]

  3. Third Call: append_to_list_mutable_default(3, [])

    • Behavior: An explicit empty list [] is provided as target_list.

    • Action: Appends 3 to this new list.

    • Result: The provided list becomes [3].

    • Output: [3]

  4. Fourth Call: append_to_list_mutable_default(4)

    • Behavior: No target_list is provided, so the default list (currently [1, 2]) is used again.

    • Action: Appends 4 to the default list.

    • Result: The default list becomes [1, 2, 4].

    • Output: [1, 2, 4]

2. What is the issue with using mutable default arguments in Python functions?#

In Python, default argument values are evaluated only once at the time of function definition, not each time the function is called. If the default value is a mutable object (like a list, dictionary, or set), and it gets modified (e.g., items are added or removed), those changes persist across subsequent function calls.

Specific Issues in append_to_list:

  • State Persistence: The default list target_list retains its state between function calls. This means that modifications made in one call affect the default list in future calls.

  • Unexpected Behavior: Users of the function might expect a new list to be created each time the function is called without an explicit target_list. Instead, they inadvertently modify the same list, leading to unexpected and often buggy behavior.

Consequences:

  • Data Accumulation: The default list accumulates values across multiple function calls, which can lead to incorrect results.

  • Hard-to-Find Bugs: Since the list retains its state, bugs related to unexpected list contents can be challenging to trace and fix.

Resolution#

  1. Default Argument Set to None:

    • By setting my_list=None, we ensure that a new list is created each time the function is called without an explicit my_list.

  2. Initialization Inside the Function:

    • The function checks if my_list is None. If it is, a new empty list [] is created.

  3. Appending the Value:

    • The value is appended to the (newly created or provided) list.

  4. Returning the List:

    • The modified list is returned, ensuring that each call operates on the intended list without unintended side effects.

So we would then have the below:

  • Avoids State Persistence: Each call without an explicit my_list gets its own new list, preventing unintended accumulation of values.

  • Predictable Behavior: The function behaves as expected, providing a fresh list for each call unless a specific list is provided.

from typing import List, TypeVar

T = TypeVar("T")


def append_to_list(item, target_list: List[T] | None = None) -> List[T]:
    target_list = target_list or []
    target_list.append(item)
    return target_list
print(append_to_list(item=1))  # Output: [1]
print(append_to_list(item=2))  # Output: [1, 2]
print(append_to_list(item=3, target_list=[]))  # Output: [3]
print(append_to_list(item=4))  # Output: [1, 2, 4]
[1]
[2]
[3]
[4]

Intentional Use Of Mutable Default To Cache#

  1. We define cached_func with a mutable default argument cache={}.

  2. The first time the function is called with a new argument, it performs the computation (in this case, just squaring the number) and stores the result in the cache.

  3. On subsequent calls with the same argument, the function returns the cached result without redoing the computation.

  4. The cache persists between function calls because it’s a mutable default argument. Python creates this dictionary once when the function is defined, not each time the function is called.

class CachedFunc:
    def __init__(self):
        self.cache = {}

    def __call__(self, arg):
        if arg not in self.cache:
            self.cache[arg] = arg * arg
        return self.cache[arg]

cached_func = CachedFunc()

cached_func(4)
cached_func(4)
cached_func(5)

print(cached_func.cache)
{4: 16, 5: 25}