pythonimmutability

How to make an immutable object in Python?


Although I have never needed this, it just struck me that making an immutable object in Python could be slightly tricky. You can't just override __setattr__, because then you can't even set attributes in the __init__. Subclassing a tuple is a trick that works:

class Immutable(tuple):
    
    def __new__(cls, a, b):
        return tuple.__new__(cls, (a, b))

    @property
    def a(self):
        return self[0]
        
    @property
    def b(self):
        return self[1]

    def __str__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)
    
    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError

But then you have access to the a and b variables through self[0] and self[1], which is annoying.

Is this possible in pure Python? If not, how would I do it with a C extension? Answers that work only in Python 3 are acceptable.


Solution

  • 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.