The Pitfalls and Solutions of Mutable Default Arguments in Python Constructors

Nov 22, 2025 · Programming · 7 views · 7.8

Keywords: Python Constructor | Default Arguments | Mutable Objects

Abstract: This article provides an in-depth analysis of the shared mutable default argument issue in Python constructors. It explains the root cause, presents the standard solution using None as a sentinel value, and discusses __init__ method mechanics and best practices. Complete code examples and step-by-step explanations help developers avoid this common pitfall.

Problem Phenomenon and Root Cause Analysis

In Python programming, when using mutable objects as default arguments in the constructor __init__ method, a common pitfall occurs. Consider the following code example:

class Node:
    def __init__(self, wordList = [], adjacencyList = []):
        self.wordList = wordList
        self.adjacencyList = adjacencyList

When creating multiple Node instances using default arguments, all instances share the same list object:

>>> a = Node()
>>> b = Node()
>>> a.wordList.append("hahaha")
>>> b.wordList
['hahaha']

The root cause of this phenomenon is that Python's function default arguments are evaluated and stored when the function is defined, not each time the function is called. For mutable objects like lists, this means all calls using default arguments reference the same object.

Python Constructor Mechanism

In Python, the __init__ method is responsible for initializing newly created object instances. Although often called a constructor, strictly speaking, object creation is handled by the __new__ method, while __init__ handles initialization.

When the __init__ method in a class definition contains default arguments, these default values are computed and stored in the function's __defaults__ attribute during class definition. For mutable objects, this causes all subsequent calls to share the same reference.

Standard Solution

The standard approach to solve this problem is to use None as a sentinel value with conditional checks inside the method:

class Node:
    def __init__(self, wordList=None, adjacencyList=None):
        if wordList is None:
            self.wordList = []
        else:
            self.wordList = wordList
        if adjacencyList is None:
            self.adjacencyList = []
        else:
            self.adjacencyList = adjacencyList

The advantages of this approach include:

Deep Understanding of Default Argument Mechanism

By examining the function's __defaults__ attribute, we can visually observe changes to default arguments:

>>> class Foo:
...     def __init__(self, x=[]):
...         x.append(1)
... 
>>> Foo.__init__.__defaults__
([],)
>>> f = Foo()
>>> Foo.__init__.__defaults__
([1],)
>>> f2 = Foo()
>>> Foo.__init__.__defaults__
([1, 1],)

This clearly demonstrates how the default list is modified across multiple calls.

Best Practices and Considerations

When designing Python constructors, follow these best practices:

  1. Avoid using mutable objects as function default arguments
  2. Use None as a sentinel value and create new objects inside the function
  3. For scenarios requiring complex initialization, consider using factory methods
  4. Document this design choice clearly in team development

This pattern applies not only to lists but also to other mutable objects like dictionaries and sets. Understanding this mechanism is crucial for writing robust Python code.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.