19.4. OOP Mutability
Function and method arguments should not be mutable
Immutable Types:
int
float
complex
bool
None
str
bytes
tuple
frozenset
mappingproxy
Mutable Types:
list
set
dict
19.4.1. Problem
Let's define a class:
>>> class User:
... def __init__(self, firstname, lastname, groups=[]):
... self.firstname = firstname
... self.lastname = lastname
... self.groups = groups
Now, we create an instance of a class:
>>> mark = User('Mark', 'Watney')
>>> melissa = User('Melissa', 'Lewis')
Check groups for both Users:
>>> mark.groups
[]
>>>
>>> melissa.groups
[]
We will assign Mark Watney to three groups: admins, staff, editors:
>>> mark.groups.append('admins')
>>> mark.groups.append('staff')
>>> mark.groups.append('editors')
Now, check the groups once again:
>>> mark.groups
['admins', 'staff', 'editors']
>>>
>>> melissa.groups
['admins', 'staff', 'editors']
This is not a mistake! Both users Mark and Melissa has the same groups
despite the fact, that we set values only for Mark. This is because both
both Mark and Melissa has attribute groups
pointing to the same memory
address:
>>> hex(id(mark.groups))
'0x10e732500'
>>>
>>> hex(id(melissa.groups))
'0x10e732500'
This is the same object!
>>> from inspect import signature
>>>
>>>
>>> signature(User.__init__)
<Signature (self, firstname, lastname, groups=['admins', 'staff', 'editors'])>
>>>
>>> signature(User.__init__).parameters.get('groups').default
['admins', 'staff', 'editors']
>>>
>>> hex(id(signature(User.__init__).parameters.get('groups').default))
'0x10e732500'
19.4.2. Why
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).
Note, You should not set mutable objects as a default function argument. 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).
>>> 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
19.4.3. Rationale
The problem lays in __init__()
method signature. It consist a reference
to the mutable object: list
. Python will create a new list
instance
on class creation, not an instance creation! Therefore each user will
reference to the same list
which was created when Python interpreted class.
>>> class User:
... def __init__(self, firstname, lastname, groups=[]):
... self.firstname = firstname
... self.lastname = lastname
... self.groups = groups
However method body is not interpreted on class creation. This is done in a
runtime. Creating a new list
in method's body will instantiate a new
sequence each time the new instance is created. Consider the following code:
>>> class User:
... def __init__(self, firstname, lastname, groups=None):
... self.firstname = firstname
... self.lastname = lastname
... self.groups = groups if groups else []
None
object is a singleton, which can be reused. Also is not a problematic,
because we will not append or modify anything to the None
itself. As soon
as the new instance is created, the __init__()
body is evaluated and
self.groups
is assigned to newly created list
instance.
19.4.4. Solution
>>> class User:
... def __init__(self, firstname, lastname, groups=None):
... self.firstname = firstname
... self.lastname = lastname
... self.groups = groups if groups else []
Now, we create an instance of a class:
>>> mark = User('Mark', 'Watney')
>>> melissa = User('Melissa', 'Lewis')
Check groups for both Users:
>>> mark.groups
[]
>>>
>>> melissa.groups
[]
We will assign Mark Watney to three groups: admins, staff, editors:
>>> mark.groups.append('admins')
>>> mark.groups.append('staff')
>>> mark.groups.append('editors')
Now, check the groups once again:
>>> mark.groups
['admins', 'staff', 'editors']
>>>
>>> melissa.groups
[]
This time their addresses are differs:
>>> hex(id(mark.groups))
'0x108ca7ac0'
>>>
>>> hex(id(melissa.groups))
'0x109a88540'
And they are not the same object:
>>> from inspect import signature
>>>
>>>
>>> signature(User.__init__)
<Signature (self, firstname, lastname, groups=None)>
>>>
>>> signature(User.__init__).parameters.get('groups').default
>>>
>>> hex(id(signature(User.__init__).parameters.get('groups').default))
'0x106ef4948'
This mechanism works the same, but this time points to the immutable object which as the name says, cannot be changed, so we are safe now:
>>> hex(id(None))
'0x106ef4948'
19.4.5. 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: OOP Mutability list
# - Difficulty: easy
# - Lines: 4
# - Minutes: 3
# %% English
# 1. Create class `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 klasę `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ść
# %% 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__')
>>> mark = User('mwatney', 'Ares3')
>>> melissa = User('mlewis', 'Nasa1')
>>> assert 'firstname' in vars(mark)
>>> assert 'lastname' in vars(mark)
>>> assert 'groups' in vars(mark)
>>> assert 'firstname' in vars(melissa)
>>> assert 'lastname' in vars(melissa)
>>> assert 'groups' in vars(melissa)
>>> assert mark.groups is not melissa.groups
"""
# Create class `User`, with attributes:
# - `firstname: str` (required)
# - `lastname: str` (required)
# - `groups: list[str]` (optional)
# type: type[User]
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: OOP Mutability Randint
# - Difficulty: easy
# - Lines: 6
# - Minutes: 3
# %% English
# 1. Create class `Hero`, with attributes:
# - `name: str` (required)
# - `health: int` (optional), default: randint(50, 100)
# 2. Attributes must be set at the initialization from constructor arguments
# 3. Avoid mutable parameter problem
# 4. Użyj funkcji `randint()` z biblioteki `random`
# 5. Do not use `dataclass`
# 6. Run doctests - all must succeed
# %% Polish
# 1. Stwórz klasę `Hero`, z atrybutami:
# - `name: str` (wymagane)
# - `health: int` (opcjonalne), domyślnie: randint(50, 100)
# 2. Atrybuty mają być ustawiane przy inicjalizacji z parametrów konstruktora
# 3. Uniknij problemu mutowalnych parametrów
# 4. Użyj funkcji `randint()` z biblioteki `random`
# 5. Nie używaj `dataclass`
# 6. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from random import seed
>>> seed(0)
>>> from inspect import isclass
>>> assert isclass(Hero)
>>> assert hasattr(Hero, '__annotations__')
>>> warrior = Hero('Warrior')
>>> assert warrior.name == 'Warrior'
>>> assert warrior.health == 74
>>> mage = Hero('Mage')
>>> assert mage.name == 'Mage'
>>> assert mage.health == 98
>>> rouge = Hero('Rouge', health=76)
>>> assert rouge.name == 'Rouge'
>>> assert rouge.health == 76
>>> cleric = Hero('Cleric', health=52)
>>> assert cleric.name == 'Cleric'
>>> assert cleric.health == 52
"""
from random import randint
# Create class `User`, with attributes:
# - `name: str` (required)
# - `health: int` (optional), default: randint(50, 100)
# Do not use `dataclass`
# type: type[Hero]
class Hero:
...