4. Creating Immutable Classes In Python
By Bernd Klein. Last modified: 19 Feb 2024.
Why do we need 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:
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
Enjoying this page? We offer live Python training courses covering the content of this site.
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
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses