Mutable Defaults and How to Fix them

Most people know the issue with mutable defaults in Python. But what’s the best way to fix it?

The issue

class User:
    def __init__(self, name: str, emails: list[str] = []) -> None:
        self.name = name
        self.emails = emails

    def add_email(self, email: str) -> None:
        self.emails.append(email)

james = User(name="James")
james.add_email("james@gmail.com")
john = User(name="John")
# John will have the emails ['james@gmail.com'], even though we never added that email to John's list.
# That's a bug!!
print(john.emails)

In tools such as ruff, you’ll see

B006 Do not use mutable data structures for argument defaults
Found 1 error.

The issue is that the default value for strings is a mutable list. This means that if you modify the list, the default value will be modified subsequently.

Suggested fix - type annotate the list as a Sequence

from typing import Sequence

class User:
    def __init__(self, name: str, emails: Sequence[str] = []) -> None:
        self.name = name
        self.emails: list[str] = list(emails)

    def add_email(self, email: str) -> None:
        self.emails.append(email)

Typing emails as Sequence[str] promises to users and your type checker that you won’t modify the list. This allows you to use [] as a default, because you won’t ever mutate it. For example, when we assign self.email, we call list(emai) to create a new list, which is a new object.

Other possible fix - Use None as the default value

class User:
    def __init__(self, name: str, emails: list[str] | None = None) -> None:
        self.name = name
        self.emails = emails or []

    def add_email(self, email: str) -> None:
        self.emails.append(email)

This solves the problem by using None as a default. Since None is immutable, it won’t be modified when you modify the list. However, I don’t really like this solution because it is confusing to me what the difference is when you pass [] or None. In this case, there is no difference between [] or None, but it requires the user to read the code to understand that. Furthermore, since the type signature may be list[str], it is not clear where the User class would mutate the list that you pass in. The add_email method for instance, may mutate the list that you pass in, which leads to future bugs.

Sidenotes and fun facts - dataclasses vs pydantic’s approach

Mutable defaults is the reason why dataclasses ban mutable defaults. If you do this, you’ll get the error mutable default <class 'list'> for field emails is not allowed: use default_factory.

from dataclasses import dataclass

@dataclass
class UserDataclass:
    emails: list[str] = [] # kaboom


So you need to do this instead:
@dataclass
class UserDataclass2:
    emails: list[str] = field(default_factory=list) # OK

Unfortunately, dataclasses still throw an error even if you typed it as a Sequence, which is annoying.

Pydantic’s approach on the other hand, does a smart deepcopy trick, where they copy the default value whenever you create a new instance of the class. This copying avoids the mutable defaults problem, and allows you to use mutable defaults without any issues.

from pydantic import BaseModel

class UserPydantic(BaseModel):
    name: str
    emails: list[str] = [] # OK, although I would still type this as a Sequence[str] to avoid other mutation bugs

    def add_email(self, email: str) -> None:
        self.emails.append(email)

james = UserPydantic(name="James")
james.add_email("james@gmail.com")
print(james.emails)
john = UserPydantic(name="John")
print(john.emails) # [] instead of ['james@gmail.com'] that we got with the User class. So no bugs here