Object Mutability and Immutability in Python
Mutable Object vs Immutable Object
Mutable or immutable is a characteristic that is usually applied to the value of an object. Some built-in types have obvious values.
For Booleans, this is True
or False
; for numeric types, a specific number, etc. The same goes for the str
and bytes
types in Python; their value is fixed. The NoneType
has a single None
object, which is used to represent the absence of a value.
Other built-in types, such as list
, tuple
, dict
, set
, etc., are implementations of complex data structures. The "value" of these containers is a collection of elements. If an object of a type has methods for adding or removing elements, that type is called mutable. Lists, dicts, and sets have such methods for manipulating their elements. Tuples, ranges, iterators, and generators do not have such options.
A mutable object in Python is an object that can change its value but keeps its id()
. An immutable object is an object with a fixed value. If we need a new value, we need to create a new object of the same type with a new id()
.
Mutable Object Example
We can change the elements of the list after it is created, and it will remain the same object.
list_ = [1, 2] print(id(list_)) # 2233573658048 list_.append(3) print(list_) # [1, 2, 3] print(id(list_)) # 2233573658048
Immutable Object Example
Immutable types can have methods to check a value for compliance with some conditions, to convert a value to another representation, etc. But there are no methods to change the initial value of the object.
x = 5 print(id(x)) # 140725583508392 # returns a new object without changing x x1 = x.to_bytes() print(x1) # b'\x05' print(id(x1)) # 2232147521872 # name x now refers to another int object x = 10 print(x) # 10 print(id(x)) # 140725583508552
But there are many objects that, in and of themselves, have no "value" associated with them. For example, modules, functions, and user-defined classes. Can these objects be considered mutable or immutable?
Classes, Functions, and Modules
Python classes can be considered mutable in the sense that you can add, change, or remove their attributes. Custom functions also support adding attributes. This feature particularly useful for monkey patching, dynamically updating the behavior of a piece of code at run-time.
This can be demonstrated for user-defined types. You can dynamically change attributes for both class object and instance objects. This does not change their ids.
class A: def __init__(self, value): self.value = value self.attr = "attr" def method(self): print("Method") a = A([1, 2, 3]) a.method() # Method a1 = A([1, 2]) a1.method() # Method # changing the attribute of the first instance a.value = [1] print(a.value) # [1] print(a1.value) # [1, 2] # change class attribute for all A.method = lambda self: print("Hello", self) a.method() # Hello <__main__.A object at 0x0000023405697FD0> a1.method() # Hello <__main__.A object at 0x0000023407FA6B90> # deleting the attribute of the second instance del a1.attr # adding arbitrary attribute A.class_attr = "qwerty" print(a.class_attr, a.attr) # qwerty attr print(a1.class_attr) # qwerty print(a1.attr) # AttributeError: 'A' object has no attribute 'attr'
As we can see, such flexibility can lead to inconsistent behavior of objects. In the case of built-in types and standard library classes, Python has mechanisms to prevent dynamic modification of these objects to avoid unexpected behavior or breaking core functionality.
This restriction applies to both the class itself and the class instance.
# attempt to add class attribute list.new_attr = 12 # TypeError: cannot set 'new_attr' attribute of immutable type 'list' list_ = [1, 2, 3] # attempt to add instance attribute list_.new_attr = 12 # AttributeError: 'list' object has no attribute 'new_attr' # built-in functions restrict attributes too print.new_attr = 12 # AttributeError: 'builtin_function_or_method' object has no attribute 'new_attr'
You cannot change the attributes of the base class.
import sys class A: pass def foo(): pass print(sys.__class__) # <class 'module'> print(A.__class__) # <class 'type'> print(foo.__class__) # <class 'function'> sys.__class__.new_attr = 12 # TypeError: cannot set 'new_attr' attribute of immutable type 'module' A.__class__.new_attr = 12 # TypeError: cannot set 'new_attr' attribute of immutable type 'type' foo.__class__.new_attr = 12 # TypeError: cannot set 'new_attr' attribute of immutable type 'function'
As for a module, its attributes are variables, functions, classes defined in the module.
Modules support setting arbitrary attributes and modifying existing attributes using dot notation.
import sys sys.attr = 12 print(sys.attr) # 12 # module attributes attrs = dir(sys) print(attrs) # [..., 'attr', ...]
In the case of the sys module, the ability to change module attributes allows us to capture and redirect output (sys.stdout
, sys.stderr
).
import sys sys.stdout = open('filename.txt', 'w') print('Some text') sys.stdout.close() # reset to original sys.stdout = sys.__stdout__
Python objects also have special attributes. There are common attributes that are inherited from the common base class object
. For example, __class__
, __doc__
, __hash__
. There are special attributes added to classes by the type
metaclass. For example, __name__
, __module__
, __dict__
. There are special attributes defined for objects of a certain type. Usually, we don't need to change these attributes.
But there is a group of special methods that are intended to be overridden. The most known of these is __init__
, which is used to customize an instance of a class.
On the other hand, Python has options to enforce type immutability. We can use special methods or descriptors for attribute lookup: __slots__
, __get__
, __set__
, __delete__
, __getattr__
, __getattribute__
, __setattr__
, __delattr__
, and @property
.
References: