How to make an immutable object in Python?
Using a Frozen Dataclass
For Python 3.7+ you can use a Data Class with a frozen=True
option, which is a very pythonic and maintainable way to do what you want.
It would look something like that:
from dataclasses import dataclass
@dataclass(frozen=True)
class Immutable:
a: Any
b: Any
As type hinting is required for dataclasses' fields, I have used Any from the typing
module.
Reasons NOT to use a Namedtuple
Before Python 3.7 it was frequent to see namedtuples being used as immutable objects. It can be tricky in many ways, one of them is that the __eq__
method between namedtuples does not consider the objects' classes. For example:
from collections import namedtuple
ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])
obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)
obj1 == obj2 # will be True
As you see, even if the types of obj1
and obj2
are different, even if their fields' names are different, obj1 == obj2
still gives True
. That's because the __eq__
method used is the tuple's one, which compares only the values of the fields given their positions. That can be a huge source of errors, specially if you are subclassing these classes.
How to make class immutable in python?
You can override __setattr__
to either prevent any changes:
def __setattr__(self, name, value):
raise AttributeError('''Can't set attribute "{0}"'''.format(name))
or prevent adding new attributes:
def __setattr__(self, name, value):
if not hasattr(self, name):
raise AttributeError('''Can't set attribute "{0}"'''.format(name))
# Or whatever the base class is, if not object.
# You can use super(), if appropriate.
object.__setattr__(self, name, value)
You can also replace hasattr
with a check against a list of allowed attributes:
if name not in list_of_allowed_attributes_to_change:
raise AttributeError('''Can't set attribute "{0}"'''.format(name))
Another approach is to use properties instead of plain attributes:
class A(object):
def __init__(self, first, second):
self._first = first
self._second = second
@property
def first(self):
return self._first
@property
def second(self):
return self._second
Ways to make a class immutable in Python
If the old trick of using __slots__
does not fit you, this, or some variant of thereof can do:
simply write the __setattr__
method of your metaclass to be your guard. In this example, I prevent new attributes of being assigned, but allow modification of existing ones:
def immutable_meta(name, bases, dct):
class Meta(type):
def __init__(cls, name, bases, dct):
type.__setattr__(cls,"attr",set(dct.keys()))
type.__init__(cls, name, bases, dct)
def __setattr__(cls, attr, value):
if attr not in cls.attr:
raise AttributeError ("Cannot assign attributes to this class")
return type.__setattr__(cls, attr, value)
return Meta(name, bases, dct)
class A:
__metaclass__ = immutable_meta
b = "test"
a = A()
a.c = 10 # this works
A.c = 20 # raises valueError
Make an object immutable
To make a mutable object immutable, all its mutable containers must be replaced by their immutable counterparts. A dictionary where all values are immutable themselves can be made immutable trivially.
Adapting the example from the defaultdict(list)
documentation:
import collections as coll
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = coll.defaultdict(tuple)
for k, v in s:
d[k] = d[k] + (v,)
print(d)
# prints
defaultdict(<class 'tuple'>, {'yellow': (1, 3), 'blue': (2, 4), 'red': (1,)})
The keys
in our defaultdict(tuple)
are immutable (strings
), and values as well (tuple
), as opposed to a defaultdict(list)
.
To freeze this dictionary:
def dict_freeze(d):
# This is the trivial one-line function
# It assumes the values are of immutable types, i.e. no lists.
# It unpacks dict items, sorts and converts to tuple
# sorting isn't strictly necessary, but dictionaries don't preserve order
# thus we could end up with the following:
# d = {'a': 1, 'b': 2} could get frozen as either of the following
# (('a', 1), ('b', 2)) != (('b', 2), ('a', 1))
return tuple(sorted(d.items()))
frozen_d = dict_freeze(d)
print(frozen_d)
# prints
(('blue', (2, 4)), ('red', (1,)), ('yellow', (1, 3)))
Thus, I would recommend using defaultdict(tuple) instead of defaultdict(list) for this case and to freeze, just unpack, sort and convert to a tuple.
Is there any way in Python to create an immutable shallow copy of any object?
without converting to an immutable type (for example list to tuple) you can't magically convert an object to be immutable.
Immutability is not a flag or something simple like that - it is about the behaviour of the methods on the object.
You could create your own custom container that has a mutable toggle (and which effectively disables some methods when mutable is on), but Python doesn't offer anything like that out of the box.
Related Topics
Parse a .Py File, Read the Ast, Modify It, Then Write Back the Modified Source Code
How to Find Children of Nodes Using Beautifulsoup
Popen.Communicate() Throws Oserror: "[Errno 10] No Child Processes"
R and Python in One Jupyter Notebook
Why Is a List Comprehension So Much Faster Than Appending to a List
Pandas: Drop a Level from a Multi-Level Column Index
How to Construct a Timedelta Object from a Simple String
How to Get the Path of the Python Script I am Running In
Merge Lists That Share Common Elements
Simple Way to Encode a String According to a Password
Understanding Python Unicode and Linux Terminal
How Is the 'Is' Keyword Implemented in Python
Difference Between Variables Inside and Outside of _Init_()