4.6. Dataclass Mutability

4.6.1. SetUp

>>> from dataclasses import dataclass, field

4.6.2. Recap

  • Function/method definition is evaluated once, when the function is defined

  • Function/method body is evaluated each time, when the function is called

  • This is how all dynamically typed languages work (JavaScript, PHP, Ruby, Perl etc).

  • You should not set mutable objects as a default function argument

  • Problem with mutable data structures like: dict, list, set

You should not set mutable objects as a default function argument.

Problem:

>>> class User:
...     def __init__(self, firstname, lastname, groups=[]):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.groups = groups
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> melissa = User('Melissa', 'Lewis')
>>>
>>> mark.groups.append('users')
>>> mark.groups.append('staff')
>>>
>>> mark.groups
['users', 'staff']
>>>
>>> melissa.groups
['users', 'staff']

Note, you should not set mutable objects or data structures (such as list, set, dict) as a default function argument.

Solution:

>>> class User:
...     def __init__(self, firstname, lastname, groups=None):
...         if groups is None:
...             groups = []
...         self.firstname = firstname
...         self.lastname = lastname
...         self.groups = groups
>>>
>>> mark = User('Mark', 'Watney')
>>> melissa = User('Melissa', 'Lewis')
>>>
>>> mark.groups.append('users')
>>> mark.groups.append('staff')
>>>
>>> mark.groups
['users', 'staff']
>>>
>>> melissa.groups
[]

Why:

>>> class User:
...     def __init__(self, firstname, lastname, groups=print(1)):
...         print(2)
...         self.firstname = firstname
...         self.lastname = lastname
...         self.groups = groups
1
>>>
>>>
>>> mark = User('Mark', 'Watney')
2
>>>
>>> melissa = User('Melissa', 'Lewis')
2

Function/method definition is evaluated once, when the function is defined. Function/method body is evaluated each time, when the function is called. This is how all dynamically typed languages work (including JavaScript, PHP, Ruby, Perl etc).

4.6.3. Problem

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = []
...
Traceback (most recent call last):
ValueError: mutable default <class 'list'> for field groups is not allowed: use default_factory

4.6.4. Solution

  • field(default_factory=lambda: [])

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = field(default_factory=lambda: [])

Usage:

>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups=[])

4.6.5. Default Values

  • field(default_factory=lambda: ['user', 'staff', 'admins'])

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = field(default_factory=lambda: ['user', 'staff', 'admins'])

Usage:

>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups=['user', 'staff', 'admins'])

4.6.6. Default Factory

  • field(default_factory=default_groups)

>>> def default_groups():
...     return ['user', 'staff', 'admins']
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = field(default_factory=default_groups)

Usage:

>>> mark = User('Mark', 'Watney')
>>> print(mark)
User(firstname='Mark', lastname='Watney', groups=['user', 'staff', 'admins'])

4.6.7. Short Notation

  • field(default_factory=list)

  • list is a callable object

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[str] = field(default_factory=list)

4.6.8. Use Case - 1

SetUp:

>>> from random import randint, seed
>>> seed(0)

Problem:

>>> @dataclass
... class Hero:
...     name: str
...     health: int = randint(50, 100)
>>>
>>>
>>> mark = Hero('Mark')
>>> melissa = Hero('Melissa')
>>> rick = Hero('Rick')
>>>
>>> mark.health
74
>>> melissa.health
74
>>> rick.health
74

Solution:

>>> @dataclass
... class Hero:
...     name: str
...     health: int = field(default_factory=lambda: randint(50, 100))
>>>
>>>
>>> mark = Hero('Mark')
>>> melissa = Hero('Melissa')
>>> rick = Hero('Rick')
>>>
>>> mark.health
98
>>> melissa.health
76
>>> rick.health
52

4.6.9. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Dataclass Mutability list
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% English
# 1. Create dataclass `User`, with attributes:
#    - `firstname: str` (required)
#    - `lastname: str` (required)
#    - `groups: list[str]` (optional)
# 2. Attributes must be set at the initialization from constructor arguments
# 3. Avoid mutable parameter problem
# 4. Run doctests - all must succeed

# %% Polish
# 1. Stwórz dataklasę `User`, z atrybutami:
#    - `firstname: str` (wymagane)
#    - `lastname: str` (wymagane)
#    - `groups: list[str]` (opcjonalne)
# 2. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
# 3. Uniknij problemu mutowalnych parametrów
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `field(default_factory=list)`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass

>>> assert isclass(User)
>>> assert hasattr(User, '__annotations__')

>>> assert 'firstname' in User.__dataclass_fields__
>>> assert 'lastname' in User.__dataclass_fields__
>>> assert 'groups' in User.__dataclass_fields__

>>> mark = User('mwatney', 'Ares3')
>>> assert mark.firstname == 'mwatney'
>>> assert mark.lastname == 'Ares3'
>>> assert mark.groups == []

>>> melissa = User('mlewis', 'Nasa1')
>>> assert melissa.firstname == 'mlewis'
>>> assert melissa.lastname == 'Nasa1'
>>> assert melissa.groups == []

>>> assert id(mark.groups) != id(melissa.groups)
"""
from dataclasses import dataclass, field


# Create class `User`, with attributes:
# - `firstname: str` (required)
# - `lastname: str` (required)
# - `groups: list[str]` (optional)
# type: type[User]
@dataclass
class User:
    ...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Dataclass Mutability list[str]
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% English
# 1. Create dataclass `User`, with attributes:
#    - `firstname: str` (required)
#    - `lastname: str` (required)
#    - `groups: list[str]` (optional), default: ['user', 'staff']
# 2. Attributes must be set at the initialization from constructor arguments
# 3. Avoid mutable parameter problem
# 4. Run doctests - all must succeed

# %% Polish
# 1. Stwórz dataklasę `User`, z atrybutami:
#    - `firstname: str` (wymagane)
#    - `lastname: str` (wymagane)
#    - `groups: list[str]` (opcjonalne), domyślnie: ['user', 'staff']
# 2. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
# 3. Uniknij problemu mutowalnych parametrów
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `field(default_factory=lambda: [...])`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass

>>> assert isclass(User)
>>> assert hasattr(User, '__annotations__')

>>> assert 'firstname' in User.__dataclass_fields__
>>> assert 'lastname' in User.__dataclass_fields__
>>> assert 'groups' in User.__dataclass_fields__

>>> mark = User('mwatney', 'Ares3')
>>> assert mark.firstname == 'mwatney'
>>> assert mark.lastname == 'Ares3'
>>> assert mark.groups == ['user', 'staff']

>>> melissa = User('mlewis', 'Nasa1')
>>> assert melissa.firstname == 'mlewis'
>>> assert melissa.lastname == 'Nasa1'
>>> assert melissa.groups == ['user', 'staff']

>>> assert id(mark.groups) != id(melissa.groups)
"""
from dataclasses import dataclass, field


# Create dataclass `User`, with attributes:
# - `firstname: str` (required)
# - `lastname: str` (required)
# - `groups: list[str]` (optional), default: ['user', 'staff']
# type: type[User]
@dataclass
class User:
    ...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Dataclass Mutability Datetime
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% English
# 1. Create dataclass `User`, with attributes:
#    - `firstname: str` (required)
#    - `lastname: str` (required)
#    - `since: datetime` (optional), default: current time
# 2. Use `datetime.now()` as default value for `since` attribute
# 3. Attributes must be set at the initialization from constructor arguments
# 4. Avoid mutable parameter problem
# 5. Run doctests - all must succeed

# %% Polish
# 1. Stwórz dataklasę `User`, z atrybutami:
#    - `firstname: str` (wymagane)
#    - `lastname: str` (wymagane)
#    - `since: datetime` (optional), domyślnie: obecny czas
# 2. Użyj `datetime.now()` jako domyślnej wartości dla atrybutu `since`
# 3. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
# 4. Uniknij problemu mutowalnych parametrów
# 5. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `field(default_factory=...)`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass

>>> assert isclass(User)
>>> assert hasattr(User, '__annotations__')

>>> assert 'firstname' in User.__dataclass_fields__
>>> assert 'lastname' in User.__dataclass_fields__
>>> assert 'since' in User.__dataclass_fields__

>>> mark = User('mwatney', 'Ares3')
>>> assert mark.firstname == 'mwatney'
>>> assert mark.lastname == 'Ares3'
>>> assert isinstance(mark.since, datetime)

>>> melissa = User('mlewis', 'Nasa1')
>>> assert melissa.firstname == 'mlewis'
>>> assert melissa.lastname == 'Nasa1'
>>> assert isinstance(melissa.since, datetime)

>>> assert id(mark.since) != id(melissa.since)
"""
from dataclasses import dataclass, field
from datetime import datetime


# Create dataclass `User`, with attributes:
# - `firstname: str` (required)
# - `lastname: str` (required)
# - `since: datetime` (optional), default: current time
# Use `datetime.now()` as default value for `since` attribute
# type: type[User]
@dataclass
class User:
    ...