How to Make an Immutable Object in Python

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



Leave a reply



Submit