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.
The easiest way to do this is using __slots__
:
class A(object):
__slots__ = []
Instances of A
are immutable now, since you can't set any attributes on them.
If you want the class instances to contain data, you can combine this with deriving from tuple
:
from operator import itemgetter
class Point(tuple):
__slots__ = []
def __new__(cls, x, y):
return tuple.__new__(cls, (x, y))
x = property(itemgetter(0))
y = property(itemgetter(1))
p = Point(2, 3)
p.x
# 2
p.y
# 3
Edit: If you want to get rid of indexing either, you can override __getitem__()
:
class Point(tuple):
__slots__ = []
def __new__(cls, x, y):
return tuple.__new__(cls, (x, y))
@property
def x(self):
return tuple.__getitem__(self, 0)
@property
def y(self):
return tuple.__getitem__(self, 1)
def __getitem__(self, item):
raise TypeError
Note that you can't use operator.itemgetter
for the properties in thise case, since this would rely on Point.__getitem__()
instead of tuple.__getitem__()
. Fuerthermore this won't prevent the use of tuple.__getitem__(p, 0)
, but I can hardly imagine how this should constitute a problem.
I don't think the "right" way of creating an immutable object is writing a C extension. Python usually relies on library implementers and library users being consenting adults, and instead of really enforcing an interface, the interface should be clearly stated in the documentation. This is why I don't consider the possibility of circumventing an overridden __setattr__()
by calling object.__setattr__()
a problem. If someone does this, it's on her own risk.
Yet another solution I just thought of: The simplest way to get the same behaviour as your original code is
Immutable = collections.namedtuple("Immutable", ["a", "b"])
It does not solve the problem that attributes can be accessed via [0]
etc., but at least it's considerably shorter and provides the additional advantage of being compatible with pickle
and copy
.
namedtuple
creates a type similar to what I described in this answer, i.e. derived from tuple
and using __slots__
. It is available in Python 2.6 or above.