Object Mutability and Immutability in Python

Object Mutability and Immutability in Python.

Table of Contents

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:

  1. Docs: Built-in Types
  2. Docs: Special attributes
  3. Docs: Basic object customization. Magic methods.
  4. Docs: Customizing attribute access
  5. Docs: Descriptor Guide
  6. Wiki: Monkey patch

Popular posts from this blog