python-course.eu

4. Creating Immutable Classes In Python

By Bernd Klein. Last modified: 19 Feb 2024.

Why do we need immutable classes?

Motionless pantomine as a symbol for immutable classes

Popular examples of immutable classes in Python include integers, floats, strings and tuples. Many functional programming languages, such as Haskell or Scala, heavily rely on immutability as a fundamental concept in their design. The reason is that immutable classes offer several advantages in software development:

  1. Thread Safety: Immutable objects are inherently thread-safe. Since their state cannot be changed after creation, multiple threads can access them concurrently without the need for locks or synchronization. This can simplify concurrent programming and reduce the risk of race conditions.

  2. Predictable Behavior: Once an immutable object is created, its state remains constant throughout its lifetime. This predictability makes it easier to reason about the behavior of the object, leading to more robust and maintainable code.

  3. Cacheability: Immutable objects can be safely cached, as their values never change. This is particularly beneficial for performance, as it allows for efficient memoization and caching strategies.

  4. Simplifies Testing: Since the state of an immutable object doesn't change, testing becomes simpler. You don't need to consider different states or mutation scenarios, making it easier to write tests and verify the correctness of your code.

  5. Consistent Hashing: Immutable objects have consistent hash codes, which is essential for their use in data structures like dictionaries or sets. This ensures that objects with the same values produce the same hash code, simplifying their use in hash-based collections.

  6. Facilitates Debugging: Debugging can be easier with immutable objects because their state doesn't change. Once you identify the initial state of an object, it remains constant, making it easier to trace and understand the flow of your program.

  7. Promotes Functional Programming: Immutable objects align well with the principles of functional programming. In functional programming, functions and data are treated as separate entities, and immutability is a key concept. Immutable objects encourage a functional style of programming, leading to more modular and composable code.

  8. Prevents Unintended Changes: With mutable objects, unintended changes to the state may occur if references to the object are shared. Immutable objects eliminate this risk, as their state cannot be modified after creation.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Ways to Create Immutable Classes

Classes with Getters and no Setters

The following ImmutableRobot class implements private attributes __name and self.__brandname, which can only be read through the methods get_name and get_brandname but there is no way to change these attributes, at least no legal way:

class ImmutableRobot:
    
    def __init__(self, name, brandname):
        self.__name = name
        self.__brandname = brandname

    def get_name(self):
        return self.__name

    def get_brandname(self):
        return self.__brandname

robot = ImmutableRobot(name="RoboX", brandname="TechBot")
print(robot.get_name())      
print(robot.get_brandname()) 

OUTPUT:

RoboX
TechBot

We can rewrite the previous example by using properties and not suppling the setter methods. So logically the same as before:

class ImmutableRobot:
    def __init__(self, name, brandname):
        self.__name = name
        self.__brandname = brandname

    @property
    def name(self):
        return self.__name

    @property
    def brandname(self):
        return self.__brandname

robot = ImmutableRobot(name="RoboX", brandname="TechBot")
print(robot.name)       
print(robot.brandname) 


try:
    robot.name = "RoboY"
except AttributeError as e:
    print(e)  

try:
    robot.brandname = "NewTechBot"
except AttributeError as e:
    print(e) 

OUTPUT:

RoboX
TechBot
property 'name' of 'ImmutableRobot' object has no setter
property 'brandname' of 'ImmutableRobot' object has no setter

Using the dataclass Decorator

from dataclasses import dataclass

@dataclass(frozen=True)
class ImmutableRobot:
    name: str
    brandname: str

# Example usage:
robot = ImmutableRobot(name="RoboX", brandname="TechBot")
print(robot.name)
print(robot.brandname)

try:
    robot.name = "RoboY"
except AttributeError as e:
    print(e)

try:
    robot.brandname = "NewTechBot"
except AttributeError as e:
    print(e)

Using namedtuple from collections

Here's an alternative using namedtuple from the collections module:

from collections import namedtuple

ImmutableRobot = namedtuple('ImmutableRobot', ['name', 'brandname'])

# Example usage:
robot = ImmutableRobot(name="RoboX", brandname="TechBot")
print(robot.name)
print(robot.brandname)

# Attempting to modify attributes will raise an AttributeError
try:
    robot.name = "RoboY"
except AttributeError as e:
    print(e)

try:
    robot.brandname = "NewTechBot"
except AttributeError as e:
    print(e)

OUTPUT:

RoboX
TechBot
can't set attribute
can't set attribute

In this example, namedtuple creates a simple class with named fields, and instances of this class are immutable. Just like with dataclass, attempting to modify attributes will result in an AttributeError.

Both namedtuple and dataclass provide a concise way to create immutable classes in Python. The choice between them depends on your specific needs and preferences. namedtuple is more lightweight, while dataclass offers additional features and customization options.

__slots__

Slots have nothing to do with creating an immutable class. Yet, it can be mistaken. With the aid of __slots__ we set the number of attributes to a fixed set. In other words: The __slots__ attribute in Python is used to explicitly declare data members (attributes) in a class. It restricts the creation of new attributes in instances of the class, allowing only the attributes specified in __slots__ to be defined. The attributes themselves can change of course, so the class can be mutable, but cannot dynamically extended with additional attributes. Though the main benefit of __slots__ is the fact that we can significantly reduce the memory overhead associated with each instance of the class. Traditional classes store attributes in a dynamic dictionary, which consumes extra memory. With __slots__, attribute names are stored in a tuple, and the instance directly allocates memory for these attributes.

Yet, when we use dataclass(frozen=True) no new attributes can be dynamically added:

class ImmutableRobot:
    __slots__ = ('__name', '__brandname')

    def __init__(self, name, brandname):
        self.__name = name
        self.__brandname = brandname

    @property
    def name(self):
        return self.__name

    @property
    def brandname(self):
        return self.__brandname

robot = ImmutableRobot(name="RoboX", brandname="TechBot")
print(robot.name)       
print(robot.brandname) 

By using __slots__, you explicitly declare the attributes that instances of the class will have, which can help reduce memory usage and improve performance, especially when creating a large number of instances.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here