How to make an immutable object in Python?
Asked Answered
C

27

256

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

Update:

As of Python 3.7, the way to go is to use the @dataclass decorator, see the newly accepted answer.

Christenachristendom answered 28/1, 2011 at 12:14 Comment(10)
Doesn't your code facilitate access to the attributes via .a and .b? That's what the properties seems to exist for after all.Inconclusive
@Sven Marnach: Yes, but [0] and [1] still work, and why would they? I don't want them. :) Maybe the idea of an immutable object with attributes is nonsense? :-)Christenachristendom
@Lennart: I initially read "then you have access to the a and b" as "you have to access the a and b", hence my comment.Inconclusive
Just another note: NotImplemented is only meant as a return value for rich comparisons. A return value for __setatt__() is rather pointless anyway, since you won't usually see it at all. Code like immutable.x = 42 will silently do nothing. You should raise a TypeError instead.Inconclusive
@Sven Marnach: OK, I was surprised, because I thought you could raise NotImplemented in this situation, but that gives a weird error. So I returned it instead, and it seemed to work. TypeError made obvious sense once I saw you used it.Christenachristendom
@Lennart: You could raise NotImplementedError, but TypeError is what a tuple raises if you try to modify it.Inconclusive
"You can't just override __setattr__, because then you can't even set attributes in the __init__" : Immutable types should be initialized into __new__, not in __init__. See : #4859629Caudell
Yeah, that doesn't really help, as it's, if it is a pure python class. You need to subclass from tuple for that to work. :)Christenachristendom
To get rid of the access through [0] and [1], can't you just override __getitem__() and make it raise an error?Colostomy
This stuff’s a bit old, but I was looking for a related issue, and I’m like, “What’s so hard about that?” @PieterNuyts, the issue is the ability to store a new value there, whether or not it’s readable. Override __setitem__(), and you’re bumping into OP’s problem—unless you just invoke the __getattribute__()/__getitem__() dunders on the base object type. On the one hand it means that no matter what you do, a determined individual can still cause mutations, but in lieu of such determination, OP is that much closer to a frozen object.Angilaangina
G
120

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.

Garrulity answered 22/11, 2019 at 10:57 Comment(5)
Because it doesn't allow a user-defined __init__ function to set attributes. The ideas of dataclass (plain old data) and immutability are orthogonal.Crescent
@Crescent sure, you can have a custom init function with data classes. python.org/dev/peps/pep-0557/#custom-init-methodSnob
After looking more into it, it seems you don't actually have to set (init=False) either, quote from the manual: "init: If true (the default), a __init__() method will be generated. If the class already defines __init__(), this parameter is ignored." docs.python.org/3/library/dataclasses.htmlSnob
For those looking a more complex custom __init__ e.g. fields that depend on another, you'll need to rely on __post_init__. See https://mcmap.net/q/111370/-how-to-set-the-value-of-dataclass-field-in-__post_init__-when-frozen-trueTirpitz
@Crescent A quick google on managing a custom __init__ or __post_init__ with a frozen dataclass gives lots of possibilities.Lovelovebird
I
134

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.

Inconclusive answered 28/1, 2011 at 13:39 Comment(16)
The advantage of this variant compared to hand-written analog (even on Python 2.5 (using verbose parameter to namedtuple the code is easily generated)) is the single interface/implementation of a namedtuple is preferrable to dozens ever so slightly different hand-written interfaces/implementations that do almost the same thing.Durr
OK, you get the "best answer", because it's the easiest way of doing it. Sebastian gets the bounty for giving a short Cython implementation. Cheers!Christenachristendom
Another characteristic of immutable objects is that when you pass them as a parameter through a function, they are copied by value, rather than another reference being made. Would namedtuples be copied by value when passed through functions?Candlestick
@hlin117: Every parameter is passed as a reference to an object in Python, regardless of whether it is mutable or immutable. For immutable objects, it would be particularly pointless to make a copy – since you can't change the object anyway, you may just as well pass a reference to the original object.Inconclusive
Can you use namedtuple internally inside the class instead of instantiating the object externally? I'm very new to python but the advantage to your other answer is that I can have a class hide the details and also have the power of things like optional parameters. If I only look at this answer it seems like I need to have everything which uses my class instantiate named tuples. Thank you for both answers.Swashbuckler
A huge downside of nametuple is that it its really a pain to add methods to this (e.g. to_json, ... from_sql()). Its possible by assigning to the returned class, but it doesn't lead to beautiful code.Twiggy
@Twiggy You can derive from the type – just make sure to add __slots__ to the derived type so it does not inadvertently become mutable.Inconclusive
Still this does not look like a sufficiently optimal solution. For example, I want to create a class whose immutable instances are just integers that behave differently (you cannot add them, if you multiply them they get added, etc.). I cannot subclass int according to normal practices (LSP), because i change the behaviour. If i use singletons (singleton tuples) they would allow to store any object inside, while i only want to store integers. It would be nice to have something like named tuples where every field would have a specific (immutable) type.Phototaxis
@Phototaxis What you are asking for is static typing, and that's completely unrelated to this question and this answer. While you can override __new__() and check for the type of the single member that is passed in, this feels out of place to me in Python. If you want static typing, just don't use a dynamically typed language.Inconclusive
@SvenMarnach, IMO my comment is about the inability to create custom immutable classes that internally are just like int. I am not asking for any more static typing than what is already in Python (int objects cannot store anything other than an integer, for example).Phototaxis
@Phototaxis That's playing with words. int objects don't store integers; they are integers. In contrast, what you are trying to do is storing an object that gets passed in to the constructor of a custom class, and you want to make sure that this custom class can only store integers. You can do this by adding a static type check, if this is what you want to do. It's usually considered unidiomatic in Python, but if you think you have good reasons to do this, just go ahead – I don't know enough about your use case to tell whether that's a good idea.Inconclusive
@SvenMarnach, i do want integers, with different methods. I want my objects to be integers (with different behaviour). I even suspect i can achieve what i want by inheriting from int instead of a named tuple, but i am too indoctrinated by the LSP and other "principles".Phototaxis
@Phototaxis That's not really integers anymore. There are also technical problems with deriving from int, apart from the surprising behaviour. When using built-in methods (like addition etc), you can't really predict whether they will return your custom type or standard integers; this is neither documented, nor consistent across the builtin Python types. So you would need to override all methods to make the new type useful, so it becomes questionable why you should derive from int in the first place. Just add a static type check to __new__() if you think this is what you need.Inconclusive
Despite preserving equality (Immutable(1,2) == Immutable(1,2) results True), and being immutable, the objects don't seem to share the memory (Immutable(1,2) is Immutable(1,2) results False), like tuples do ((1,2) is (1,2) results True).Catherincatherina
@olyk It's completely up to the Python runtime to decide whether objects share memory. Usually they don't in CPython. Try, e.g., a = (1, 2); a is (1, 2), and it should return False. This behaviour may change at any time, and you shouldn't rely on it in any way.Inconclusive
Nice, however this prevent intellisense from properly workingWhetstone
G
120

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.

Garrulity answered 22/11, 2019 at 10:57 Comment(5)
Because it doesn't allow a user-defined __init__ function to set attributes. The ideas of dataclass (plain old data) and immutability are orthogonal.Crescent
@Crescent sure, you can have a custom init function with data classes. python.org/dev/peps/pep-0557/#custom-init-methodSnob
After looking more into it, it seems you don't actually have to set (init=False) either, quote from the manual: "init: If true (the default), a __init__() method will be generated. If the class already defines __init__(), this parameter is ignored." docs.python.org/3/library/dataclasses.htmlSnob
For those looking a more complex custom __init__ e.g. fields that depend on another, you'll need to rely on __post_init__. See https://mcmap.net/q/111370/-how-to-set-the-value-of-dataclass-field-in-__post_init__-when-frozen-trueTirpitz
@Crescent A quick google on managing a custom __init__ or __post_init__ with a frozen dataclass gives lots of possibilities.Lovelovebird
I
97

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.

Inconclusive answered 28/1, 2011 at 12:17 Comment(10)
Wouldn't it be a better idea to use a tuple here, __slots__ = (), rather than __slots__ = []? (Just clarifying)Humectant
@sukhbir: I think this does not matter at all. Why would you prefer a tuple?Inconclusive
@Sven: I agree it wouldn't matter (except the speed part, which we can ignore), but I thought of it this way: __slots__ is not going to be changed right? It's purpose is to identify for once which attributes can be set. So doesn't a tuple seem a much natural choice in such a case?Humectant
But with an empty __slots__ I can't set any attributes. And if I have __slots__ = ('a', 'b') then the a and b attributes are still mutable.Christenachristendom
But your solution is better than overriding __setattr__ so it's an improvement over mine. +1 :)Christenachristendom
What is the advantage compared to Point = namedtuple("Point", "x y")?Durr
@J.F. Sebastian: Full control over the code, and it works in Python 2.5.Inconclusive
@J.F.Sebastian, another advantage is that because it's a class, you can write member functions. I don't think that's possible with namedtuple. Example, imagine a type for Complex numbers, where you'd like to have member functions for converting from internal representation to either Cartesian, i.e., (re, im), or polar, i.e., (mag, arg).Aftermath
@Durr One advantage of this, over namedtuple, is that you can specify default values in the __new__ signatureAhmedahmedabad
@JamesSchinner there is typing.NamedTuple which allows to specify default values.Durr
D
63

..howto do it "properly" in C..

You could use Cython to create an extension type for Python:

cdef class Immutable:
    cdef readonly object a, b
    cdef object __weakref__ # enable weak referencing support

    def __init__(self, a, b):
        self.a, self.b = a, b

It works both Python 2.x and 3.

Tests

# compile on-the-fly
import pyximport; pyximport.install() # $ pip install cython
from immutable import Immutable

o = Immutable(1, 2)
assert o.a == 1, str(o.a)
assert o.b == 2

try: o.a = 3
except AttributeError:
    pass
else:
    assert 0, 'attribute must be readonly'

try: o[1]
except TypeError:
    pass
else:
    assert 0, 'indexing must not be supported'

try: o.c = 1
except AttributeError:
    pass
else:
    assert 0, 'no new attributes are allowed'

o = Immutable('a', [])
assert o.a == 'a'
assert o.b == []

o.b.append(3) # attribute may contain mutable object
assert o.b == [3]

try: o.c
except AttributeError:
    pass
else:
    assert 0, 'no c attribute'

o = Immutable(b=3,a=1)
assert o.a == 1 and o.b == 3

try: del o.b
except AttributeError:
    pass
else:
    assert 0, "can't delete attribute"

d = dict(b=3, a=1)
o = Immutable(**d)
assert o.a == d['a'] and o.b == d['b']

o = Immutable(1,b=3)
assert o.a == 1 and o.b == 3

try: object.__setattr__(o, 'a', 1)
except AttributeError:
    pass
else:
    assert 0, 'attributes are readonly'

try: object.__setattr__(o, 'c', 1)
except AttributeError:
    pass
else:
    assert 0, 'no new attributes'

try: Immutable(1,c=3)
except TypeError:
    pass
else:
    assert 0, 'accept only a,b keywords'

for kwd in [dict(a=1), dict(b=2)]:
    try: Immutable(**kwd)
    except TypeError:
        pass
    else:
        assert 0, 'Immutable requires exactly 2 arguments'

If you don't mind indexing support then collections.namedtuple suggested by @Sven Marnach is preferrable:

Immutable = collections.namedtuple("Immutable", "a b")
Durr answered 31/1, 2011 at 17:34 Comment(6)
@Lennart: Instances of namedtuple (or more precisely of the type returned by the function namedtuple()) are immutable. Definitely.Inconclusive
@Lennart Regebro: namedtuple passes all the tests (except indexing support). What requirement did I miss?Durr
Yes, you are right, I made a namedtuple type, instantiated it, and then did the test on the type instead of the instance. Heh. :-)Christenachristendom
may I ask why would one needs weak referencing here?Wakerly
@McSinyx: otherwise, the objects can't be used in weakref's collections. What exactly is __weakref__ in Python?Durr
The immutables library is a native C library with Python bindings for this behavior. See github.com/MagicStack/immutablesGamecock
I
46

Another idea would be to completely disallow __setattr__ and use object.__setattr__ in the constructor:

class Point(object):
    def __init__(self, x, y):
        object.__setattr__(self, "x", x)
        object.__setattr__(self, "y", y)
    def __setattr__(self, *args):
        raise TypeError
    def __delattr__(self, *args):
        raise TypeError

Of course you could use object.__setattr__(p, "x", 3) to modify a Point instance p, but your original implementation suffers from the same problem (try tuple.__setattr__(i, "x", 42) on an Immutable instance).

You can apply the same trick in your original implementation: get rid of __getitem__(), and use tuple.__getitem__() in your property functions.

Inconclusive answered 28/1, 2011 at 13:1 Comment(1)
I would not care about someone deliberately modifying the object using superclass' __setattr__, because the point is not to be foolproof. The point is to make it clear that it should not be modified and to prevent modification by mistake.Unwarrantable
F
20

You could create a @immutable decorator that either overrides the __setattr__ and change the __slots__ to an empty list, then decorate the __init__ method with it.

Edit: As the OP noted, changing the __slots__ attribute only prevents the creation of new attributes, not the modification.

Edit2: Here's an implementation:

Edit3: Using __slots__ breaks this code, because if stops the creation of the object's __dict__. I'm looking for an alternative.

Edit4: Well, that's it. It's a but hackish, but works as an exercise :-)

class immutable(object):
    def __init__(self, immutable_params):
        self.immutable_params = immutable_params

    def __call__(self, new):
        params = self.immutable_params

        def __set_if_unset__(self, name, value):
            if name in self.__dict__:
                raise Exception("Attribute %s has already been set" % name)

            if not name in params:
                raise Exception("Cannot create atribute %s" % name)

            self.__dict__[name] = value;

        def __new__(cls, *args, **kws):
            cls.__setattr__ = __set_if_unset__

            return super(cls.__class__, cls).__new__(cls, *args, **kws)

        return __new__

class Point(object):
    @immutable(['x', 'y'])
    def __new__(): pass

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2) 
p.x = 3 # Exception: Attribute x has already been set
p.z = 4 # Exception: Cannot create atribute z
Fidole answered 28/1, 2011 at 12:20 Comment(3)
Making a (class?) decorator or metaclass out of the solution is indeed a good idea, but the question is what the solution is. :)Christenachristendom
object.__setattr__() breaks it #4828580Durr
Indeed. I just carried on as an exercise on decorators.Fidole
C
12

I don't think it is entirely possible except by using either a tuple or a namedtuple. No matter what, if you override __setattr__() the user can always bypass it by calling object.__setattr__() directly. Any solution that depends on __setattr__ is guaranteed not to work.

The following is about the nearest you can get without using some sort of tuple:

class Immutable:
    __slots__ = ['a', 'b']
    def __init__(self, a, b):
        object.__setattr__(self, 'a', a)
        object.__setattr__(self, 'b', b)
    def __setattr__(self, *ignored):
        raise NotImplementedError
    __delattr__ = __setattr__

but it breaks if you try hard enough:

>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2

but Sven's use of namedtuple is genuinely immutable.

Update

Since the question has been updated to ask how to do it properly in C, here's my answer on how to do it properly in Cython:

First immutable.pyx:

cdef class Immutable:
    cdef object _a, _b

    def __init__(self, a, b):
        self._a = a
        self._b = b

    property a:
        def __get__(self):
            return self._a

    property b:
        def __get__(self):
            return self._b

    def __repr__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

and a setup.py to compile it (using the command setup.py build_ext --inplace:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("immutable", ["immutable.pyx"])]

setup(
  name = 'Immutable object',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

Then to try it out:

>>> from immutable import Immutable
>>> p = Immutable(2, 3)
>>> p
<Immutable 2, 3>
>>> p.a = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> object.__setattr__(p, 'a', 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> p.a, p.b
(2, 3)
>>>      
Chaperone answered 28/1, 2011 at 14:36 Comment(1)
Thanks for the Cython code, Cython is awesome. J.F. Sebastians implementation with the readonly is neater and arrived first though, so he gets the bounty.Christenachristendom
G
8

I've made immutable classes by overriding __setattr__, and allowing the set if the caller is __init__:

import inspect
class Immutable(object):
    def __setattr__(self, name, value):
        if inspect.stack()[2][3] != "__init__":
            raise Exception("Can't mutate an Immutable: self.%s = %r" % (name, value))
        object.__setattr__(self, name, value)

This isn't quite enough yet, since it allows anyone's __init__ to change the object, but you get the idea.

Goddard answered 28/1, 2011 at 12:51 Comment(6)
object.__setattr__() breaks it #4828580Durr
Using stack inspection to ensure the caller is __init__ is not very satisfying.Scrutineer
Love this -- thanks @Ned -- works perfectly for my code. I think @TimurTimergalin's answer might be more modern, but this works. I came here to post the same, but with if inspect.currentframe().f_back.f_code.co_name in ('__init__', '__new__') instead. And it will still work if your object needs multiple metaclasses which @TimurTimergalin's approach won't necessarily handle.Pendulum
@Ned Batchelder -- But shouldn't it be "==" not "!=" and also there needs to be a way so that __init__ can call a function that then calls this.Pendulum
Ended up going with for f in inspect.stack(): if f.frame.f_code.co_name in ('__init__', '__new__'): ... do something ... it does still allow other init methods to change this, but it was good enough fro what I needed.Pendulum
comparing f.frame.f_locals['self'].__class_ to self.__class__ should prevent other __init__ methods from altering the object. Yes, object.__setattr__... will still work, but someone who does that should know what they're doing.Pendulum
E
7

Here's an elegant solution:

class Immutable(object):
    def __setattr__(self, key, value):
        if not hasattr(self, key):
            super().__setattr__(key, value)
        else:
            raise RuntimeError("Can't modify immutable object's attribute: {}".format(key))

Inherit from this class, initialize your fields in the constructor, and you'e all set.

Eddings answered 29/6, 2018 at 4:32 Comment(1)
but with this logic it possible to assign new attributes to the objectEanore
I
5

In addition to the excellent other answers I like to add a method for python 3.4 (or maybe 3.3). This answer builds upon several previouse answers to this question.

In python 3.4, you can use properties without setters to create class members that cannot be modified. (In earlier versions assigning to properties without a setter was possible.)

class A:
    __slots__=['_A__a']
    def __init__(self, aValue):
      self.__a=aValue
    @property
    def a(self):
        return self.__a

You can use it like this:

instance=A("constant")
print (instance.a)

which will print "constant"

But calling instance.a=10 will cause:

AttributeError: can't set attribute

Explaination: properties without setters are a very recent feature of python 3.4 (and I think 3.3). If you try to assign to such a property, an Error will be raised. Using slots I restrict the membervariables to __A_a (which is __a).

Problem: Assigning to _A__a is still possible (instance._A__a=2). But if you assign to a private variable, it is your own fault...

This answer among others, however, discourages the use of __slots__. Using other ways to prevent attribute creation might be preferrable.

Imaret answered 2/7, 2015 at 10:29 Comment(6)
property is available on Python 2 too (look at the code in the question itself). It does not create an immutable object, try the tests from my answer e.g., instance.b = 1 creates a new b attribute.Durr
Right, the question is really how to prevent doing A().b = "foo" ie not allowing setting new attributes.Christenachristendom
Propertis without a setter raise an error in python 3.4 if you try to assigne to that property. In earlier versions the setter was generated implicitely.Imaret
@Lennart: My solution is an answer to a subset of use-cases for immutable objects and an addition to previous answers. One reason I might want an immutable object is so that I can make it hashable, for which case my solution might works. But you are correct, this is not an immutable object.Imaret
@j-f-sebastian: Changed my answer to use slots for preventing attribute creation. What is new in my answer compared to other answers, is that I use python3.4's properties to avoid changing existent attributes. While the same is achieved in previose answers, my code is shorther because of the change in the behaviour of properties.Imaret
Yeah, slots + properties is good enough for most usecases, even if it's not truly immutable.Christenachristendom
H
5

So, I am writing respective of python 3:

I) with the help of data class decorator and set frozen=True. we can create immutable objects in python.

for this need to import data class from data classes lib and needs to set frozen=True

ex.

from dataclasses import dataclass

@dataclass(frozen=True)
class Location:
    name: str
    longitude: float = 0.0
    latitude: float = 0.0

o/p:

>>> l = Location("Delhi", 112.345, 234.788)
>>> l.name
'Delhi'
>>> l.longitude
112.345
>>> l.latitude
234.788
>>> l.name = "Kolkata"
dataclasses.FrozenInstanceError: cannot assign to field 'name'
>>> 

Source: https://realpython.com/python-data-classes/

Homeostasis answered 22/11, 2019 at 12:26 Comment(0)
S
4

If you are interested in objects with behavior, then namedtuple is almost your solution.

As described at the bottom of the namedtuple documentation, you can derive your own class from namedtuple; and then, you can add the behavior you want.

For example (code taken directly from the documentation):

class Point(namedtuple('Point', 'x y')):
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

for p in Point(3, 4), Point(14, 5/7):
    print(p)

This will result in:

Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

This approach works for both Python 3 and Python 2.7 (tested on IronPython as well).
The only downside is that the inheritance tree is a bit weird; but this is not something you usually play with.

Slr answered 29/1, 2016 at 18:47 Comment(1)
Python 3.6+ supports this directly, using class Point(typing.NamedTuple):Kappa
B
4

As of Python 3.7, you can use the @dataclass decorator in your class and it will be immutable like a struct! Though, it may or may not add a __hash__() method to your class. Quote:

hash() is used by built-in hash(), and when objects are added to hashed collections such as dictionaries and sets. Having a hash() implies that instances of the class are immutable. Mutability is a complicated property that depends on the programmer’s intent, the existence and behavior of eq(), and the values of the eq and frozen flags in the dataclass() decorator.

By default, dataclass() will not implicitly add a hash() method unless it is safe to do so. Neither will it add or change an existing explicitly defined hash() method. Setting the class attribute hash = None has a specific meaning to Python, as described in the hash() documentation.

If hash() is not explicit defined, or if it is set to None, then dataclass() may add an implicit hash() method. Although not recommended, you can force dataclass() to create a hash() method with unsafe_hash=True. This might be the case if your class is logically immutable but can nonetheless be mutated. This is a specialized use case and should be considered carefully.

Here the example from the docs linked above:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand
Byrnes answered 6/9, 2018 at 12:51 Comment(1)
you need to use frozen, i.e. @dataclass(frozen=True), but it basically blocks use of __setattr__ and __delattr__ like in most of the other answers here . It just does it in a way that is compatible with the other options of dataclasses.Crosseyed
Z
4

Just Like a dict

I have an open source library where I'm doing things in a functional way so moving data around in an immutable object is helpful. However, I don't want to have to transform my data object for the client to interact with them. So, I came up with this - it gives you a dict like object thats immutable + some helper methods.

Credit to Sven Marnach in his answer for the basic implementation of restricting property updating and deleting.

import json 
# ^^ optional - If you don't care if it prints like a dict
# then rip this and __str__ and __repr__ out

class Immutable(object):

    def __init__(self, **kwargs):
        """Sets all values once given
        whatever is passed in kwargs
        """
        for k,v in kwargs.items():
            object.__setattr__(self, k, v)

    def __setattr__(self, *args):
        """Disables setting attributes via
        item.prop = val or item['prop'] = val
        """
        raise TypeError('Immutable objects cannot have properties set after init')

    def __delattr__(self, *args):
        """Disables deleting properties"""
        raise TypeError('Immutable objects cannot have properties deleted')

    def __getitem__(self, item):
        """Allows for dict like access of properties
        val = item['prop']
        """
        return self.__dict__[item]

    def __repr__(self):
        """Print to repl in a dict like fashion"""
        return self.pprint()

    def __str__(self):
        """Convert to a str in a dict like fashion"""
        return self.pprint()

    def __eq__(self, other):
        """Supports equality operator
        immutable({'a': 2}) == immutable({'a': 2})"""
        if other is None:
            return False
        return self.dict() == other.dict()

    def keys(self):
        """Paired with __getitem__ supports **unpacking
        new = { **item, **other }
        """
        return self.__dict__.keys()

    def get(self, *args, **kwargs):
        """Allows for dict like property access
        item.get('prop')
        """
        return self.__dict__.get(*args, **kwargs)

    def pprint(self):
        """Helper method used for printing that
        formats in a dict like way
        """
        return json.dumps(self,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4)

    def dict(self):
        """Helper method for getting the raw dict value
        of the immutable object"""
        return self.__dict__

Helper methods

def update(obj, **kwargs):
    """Returns a new instance of the given object with
    all key/val in kwargs set on it
    """
    return immutable({
        **obj,
        **kwargs
    })

def immutable(obj):
    return Immutable(**obj)

Examples

obj = immutable({
    'alpha': 1,
    'beta': 2,
    'dalet': 4
})

obj.alpha # 1
obj['alpha'] # 1
obj.get('beta') # 2

del obj['alpha'] # TypeError
obj.alpha = 2 # TypeError

new_obj = update(obj, alpha=10)

new_obj is not obj # True
new_obj.get('alpha') == 10 # True
Zachar answered 26/10, 2019 at 19:24 Comment(0)
N
3

This way doesn't stop object.__setattr__ from working, but I've still found it useful:

class A(object):

    def __new__(cls, children, *args, **kwargs):
        self = super(A, cls).__new__(cls)
        self._frozen = False  # allow mutation from here to end of  __init__
        # other stuff you need to do in __new__ goes here
        return self

    def __init__(self, *args, **kwargs):
        super(A, self).__init__()
        self._frozen = True  # prevent future mutation

    def __setattr__(self, name, value):
        # need to special case setting _frozen.
        if name != '_frozen' and self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__setattr__(name, value)

    def __delattr__(self, name):
        if self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__delattr__(name)

you may need to override more stuff (like __setitem__) depending on the use case.

Nonalcoholic answered 7/8, 2013 at 0:10 Comment(3)
I came up with something similar before I saw this, but used getattr so I could provide a default value for frozen. That simplified things a bit. https://mcmap.net/q/111371/-how-to-make-class-immutable-in-python-duplicateFreddyfredek
I like this approach the best, but you don't need the __new__ override. Inside __setattr__ just replace the conditional with if name != '_frozen' and getattr(self, "_frozen", False)Expectancy
Also, there is no need to freeze the class upon construction. You can freeze it at any point if you provide a freeze() function. The object will then be "freeze once". Finally, worrying about object.__setattr__ is silly, because "we're all adults here".Expectancy
A
3

Classes which inherit from the following Immutable class are immutable, as are their instances, after their __init__ method finishes executing. Since it's pure python, as others have pointed out, there's nothing stopping someone from using the mutating special methods from the base object and type, but this is enough to stop anyone from mutating a class/instance by accident.

It works by hijacking the class-creation process with a metaclass.

"""Subclasses of class Immutable are immutable after their __init__ has run, in
the sense that all special methods with mutation semantics (in-place operators,
setattr, etc.) are forbidden.

"""  

# Enumerate the mutating special methods
mutation_methods = set()
# Arithmetic methods with in-place operations
iarithmetic = '''add sub mul div mod divmod pow neg pos abs bool invert lshift
                 rshift and xor or floordiv truediv matmul'''.split()
for op in iarithmetic:
    mutation_methods.add('__i%s__' % op)
# Operations on instance components (attributes, items, slices)
for verb in ['set', 'del']:
    for component in '''attr item slice'''.split():
        mutation_methods.add('__%s%s__' % (verb, component))
# Operations on properties
mutation_methods.update(['__set__', '__delete__'])


def checked_call(_self, name, method, *args, **kwargs):
    """Calls special method method(*args, **kw) on self if mutable."""
    self = args[0] if isinstance(_self, object) else _self
    if not getattr(self, '__mutable__', True):
        # self told us it's immutable, so raise an error
        cname= (self if isinstance(self, type) else self.__class__).__name__
        raise TypeError('%s is immutable, %s disallowed' % (cname, name))
    return method(*args, **kwargs)


def method_wrapper(_self, name):
    "Wrap a special method to check for mutability."
    method = getattr(_self, name)
    def wrapper(*args, **kwargs):
        return checked_call(_self, name, method, *args, **kwargs)
    wrapper.__name__ = name
    wrapper.__doc__ = method.__doc__
    return wrapper


def wrap_mutating_methods(_self):
    "Place the wrapper methods on mutative special methods of _self"
    for name in mutation_methods:
        if hasattr(_self, name):
            method = method_wrapper(_self, name)
            type.__setattr__(_self, name, method)


def set_mutability(self, ismutable):
    "Set __mutable__ by using the unprotected __setattr__"
    b = _MetaImmutable if isinstance(self, type) else Immutable
    super(b, self).__setattr__('__mutable__', ismutable)


class _MetaImmutable(type):

    '''The metaclass of Immutable. Wraps __init__ methods via __call__.'''

    def __init__(cls, *args, **kwargs):
        # Make class mutable for wrapping special methods
        set_mutability(cls, True)
        wrap_mutating_methods(cls)
        # Disable mutability
        set_mutability(cls, False)

    def __call__(cls, *args, **kwargs):
        '''Make an immutable instance of cls'''
        self = cls.__new__(cls)
        # Make the instance mutable for initialization
        set_mutability(self, True)
        # Execute cls's custom initialization on this instance
        self.__init__(*args, **kwargs)
        # Disable mutability
        set_mutability(self, False)
        return self

    # Given a class T(metaclass=_MetaImmutable), mutative special methods which
    # already exist on _MetaImmutable (a basic type) cannot be over-ridden
    # programmatically during _MetaImmutable's instantiation of T, because the
    # first place python looks for a method on an object is on the object's
    # __class__, and T.__class__ is _MetaImmutable. The two extant special
    # methods on a basic type are __setattr__ and __delattr__, so those have to
    # be explicitly overridden here.

    def __setattr__(cls, name, value):
        checked_call(cls, '__setattr__', type.__setattr__, cls, name, value)

    def __delattr__(cls, name, value):
        checked_call(cls, '__delattr__', type.__delattr__, cls, name, value)


class Immutable(object):

    """Inherit from this class to make an immutable object.

    __init__ methods of subclasses are executed by _MetaImmutable.__call__,
    which enables mutability for the duration.

    """

    __metaclass__ = _MetaImmutable


class T(int, Immutable):  # Checks it works with multiple inheritance, too.

    "Class for testing immutability semantics"

    def __init__(self, b):
        self.b = b

    @classmethod
    def class_mutation(cls):
        cls.a = 5

    def instance_mutation(self):
        self.c = 1

    def __iadd__(self, o):
        pass

    def not_so_special_mutation(self):
        self +=1

def immutabilityTest(f, name):
    "Call f, which should try to mutate class T or T instance."
    try:
        f()
    except TypeError, e:
        assert 'T is immutable, %s disallowed' % name in e.args
    else:
        raise RuntimeError('Immutability failed!')

immutabilityTest(T.class_mutation, '__setattr__')
immutabilityTest(T(6).instance_mutation, '__setattr__')
immutabilityTest(T(6).not_so_special_mutation, '__iadd__')
Applicator answered 27/2, 2016 at 10:2 Comment(0)
K
3

The third party attr module provides this functionality.

Edit: python 3.7 has adopted this idea into the stdlib with @dataclass.

$ pip install attrs
$ python
>>> @attr.s(frozen=True)
... class C(object):
...     x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
   ...
attr.exceptions.FrozenInstanceError: can't set attribute

attr implements frozen classes by overriding __setattr__ and has a minor performance impact at each instantiation time, according to the documentation.

If you're in the habit of using classes as datatypes, attr may be especially useful as it takes care of the boilerplate for you (but doesn't do any magic). In particular, it writes nine dunder (__X__) methods for you (unless you turn any of them off), including repr, init, hash and all the comparison functions.

attr also provides a helper for __slots__.

Kraft answered 14/5, 2017 at 19:21 Comment(0)
P
3

You can override setattr and still use init to set the variable. You would use super class setattr. here is the code.

class Immutable:
    __slots__ = ('a','b')
    def __init__(self, a , b):
        super().__setattr__('a',a)
        super().__setattr__('b',b)

    def __str__(self):
        return "".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError
Presage answered 25/10, 2018 at 13:7 Comment(2)
Or just pass instead of raise NotImplementedErrorLith
It's not a good idea at all to do "pass" in __setattr__ and __delattr__ in this case. The simple reason is that if somebody assigns a value to a field/property, then they naturally expect that the field will be changed. If you want to follow the path of "least surprise" (as you should), then you have to raise an error. But I'm not sure if NotImplementedError is the right one to raise. I'd raise something like "Field/property is immutable." error... I think a custom exception should be thrown.Kirkcudbright
H
2

I needed this a little while ago and decided to make a Python package for it. The initial version is on PyPI now:

$ pip install immutable

To use:

>>> from immutable import ImmutableFactory
>>> MyImmutable = ImmutableFactory.create(prop1=1, prop2=2, prop3=3)
>>> MyImmutable.prop1
1

Full docs here: https://github.com/theengineear/immutable

Hope it helps, it wraps a namedtuple as has been discussed, but makes instantiation much simpler.

Hunk answered 4/6, 2015 at 12:50 Comment(0)
D
2

The basic solution below addresses the following scenario:

  • __init__() can be written accessing the attributes as usual.
  • AFTER that the OBJECT is frozen for attributes changes only:

The idea is to override __setattr__ method and replace its implementation each time the object frozen status is changed.

So we need some method (_freeze) which stores those two implementations and switches between them when requested.

This mechanism may be implemented inside the user class or inherited from a special Freezer class as shown below:

class Freezer:
    def _freeze(self, do_freeze=True):
        def raise_sa(*args):            
            raise AttributeError("Attributes are frozen and can not be changed!")
        super().__setattr__('_active_setattr', (super().__setattr__, raise_sa)[do_freeze])

    def __setattr__(self, key, value):        
        return self._active_setattr(key, value)

class A(Freezer):    
    def __init__(self):
        self._freeze(False)
        self.x = 10
        self._freeze()
Distract answered 16/9, 2018 at 15:15 Comment(0)
B
2

I found a way to do it without subclassing tuple, namedtuple etc. All you need to do is to disable setattr and delattr (and also setitem and delitem if you want to make a collection immutable) after the initiation:

def __init__(self, *args, **kwargs):
    # something here

    self.lock()

where lock can look like this:

@classmethod
def lock(cls):
    def raiser(*a):
        raise TypeError('this instance is immutable')

    cls.__setattr__ = raiser
    cls.__delattr__ = raiser
    if hasattr(cls, '__setitem__'):
        cls.__setitem__ = raiser
        cls.__delitem__ = raiser

So you can create class Immutable with this method and use it the way I showed.

If you don't want to write self.lock() in every single init you can make it automatically with metaclasses:

class ImmutableType(type):
    @classmethod
    def change_init(mcs, original_init_method):
        def __new_init__(self, *args, **kwargs):
            if callable(original_init_method):
                original_init_method(self, *args, **kwargs)

            cls = self.__class__

            def raiser(*a):
                raise TypeError('this instance is immutable')

            cls.__setattr__ = raiser
            cls.__delattr__ = raiser
            if hasattr(cls, '__setitem__'):
                cls.__setitem__ = raiser
                cls.__delitem__ = raiser

        return __new_init__

    def __new__(mcs, name, parents, kwargs):
        kwargs['__init__'] = mcs.change_init(kwargs.get('__init__'))
        return type.__new__(mcs, name, parents, kwargs)


class Immutable(metaclass=ImmutableType):
    pass

Test

class SomeImmutableClass(Immutable):
    def __init__(self, some_value: int):
        self.important_attr = some_value

    def some_method(self):
        return 2 * self.important_attr


ins = SomeImmutableClass(3)
print(ins.some_method())  # 6
ins.important_attr += 1  # TypeError
ins.another_attr = 2  # TypeError
Broker answered 30/8, 2021 at 16:19 Comment(3)
Well that work for two objects? You are overriding the class methods, so it would lock all instances I think?Christenachristendom
This is an amazing solution. I'm surprised it's not much higher. Maybe people don't understand metaclassesPendulum
but for some reason, I wasn't able to make this work -- all my __init__ setters were still rejected and I wen't with Ned's solution.Pendulum
S
1

An alternative approach is to create a wrapper which makes an instance immutable.

class Immutable(object):

    def __init__(self, wrapped):
        super(Immutable, self).__init__()
        object.__setattr__(self, '_wrapped', wrapped)

    def __getattribute__(self, item):
        return object.__getattribute__(self, '_wrapped').__getattribute__(item)

    def __setattr__(self, key, value):
        raise ImmutableError('Object {0} is immutable.'.format(self._wrapped))

    __delattr__ = __setattr__

    def __iter__(self):
        return object.__getattribute__(self, '_wrapped').__iter__()

    def next(self):
        return object.__getattribute__(self, '_wrapped').next()

    def __getitem__(self, item):
        return object.__getattribute__(self, '_wrapped').__getitem__(item)

immutable_instance = Immutable(my_instance)

This is useful in situations where only some instances have to be immutable (like default arguments of function calls).

Can also be used in immutable factories like:

@classmethod
def immutable_factory(cls, *args, **kwargs):
    return Immutable(cls.__init__(*args, **kwargs))

Also protects from object.__setattr__, but fallable to other tricks due to Python's dynamic nature.

Saxophone answered 1/5, 2015 at 12:1 Comment(0)
M
1

I used the same idea as Alex: a meta-class and an "init marker", but in combination with over-writing __setattr__:

>>> from abc import ABCMeta
>>> _INIT_MARKER = '_@_in_init_@_'
>>> class _ImmutableMeta(ABCMeta):
... 
...     """Meta class to construct Immutable."""
... 
...     def __call__(cls, *args, **kwds):
...         obj = cls.__new__(cls, *args, **kwds)
...         object.__setattr__(obj, _INIT_MARKER, True)
...         cls.__init__(obj, *args, **kwds)
...         object.__delattr__(obj, _INIT_MARKER)
...         return obj
...
>>> def _setattr(self, name, value):
...     if hasattr(self, _INIT_MARKER):
...         object.__setattr__(self, name, value)
...     else:
...         raise AttributeError("Instance of '%s' is immutable."
...                              % self.__class__.__name__)
...
>>> def _delattr(self, name):
...     raise AttributeError("Instance of '%s' is immutable."
...                          % self.__class__.__name__)
...
>>> _im_dict = {
...     '__doc__': "Mix-in class for immutable objects.",
...     '__copy__': lambda self: self,   # self is immutable, so just return it
...     '__setattr__': _setattr,
...     '__delattr__': _delattr}
...
>>> Immutable = _ImmutableMeta('Immutable', (), _im_dict)

Note: I'm calling the meta-class directly to make it work both for Python 2.x and 3.x.

>>> class T1(Immutable):
... 
...     def __init__(self, x=1, y=2):
...         self.x = x
...         self.y = y
...
>>> t1 = T1(y=8)
>>> t1.x, t1.y
(1, 8)
>>> t1.x = 7
AttributeError: Instance of 'T1' is immutable.

It does work also with slots ...:

>>> class T2(Immutable):
... 
...     __slots__ = 's1', 's2'
... 
...     def __init__(self, s1, s2):
...         self.s1 = s1
...         self.s2 = s2
...
>>> t2 = T2('abc', 'xyz')
>>> t2.s1, t2.s2
('abc', 'xyz')
>>> t2.s1 += 'd'
AttributeError: Instance of 'T2' is immutable.

... and multiple inheritance:

>>> class T3(T1, T2):
... 
...     def __init__(self, x, y, s1, s2):
...         T1.__init__(self, x, y)
...         T2.__init__(self, s1, s2)
...
>>> t3 = T3(12, 4, 'a', 'b')
>>> t3.x, t3.y, t3.s1, t3.s2
(12, 4, 'a', 'b')
>>> t3.y -= 3
AttributeError: Instance of 'T3' is immutable.

Note, however, that mutable attributes stay to be mutable:

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]
Mccleary answered 8/3, 2016 at 10:6 Comment(0)
A
1

One thing that's not really included here is total immutability... not just the parent object, but all the children as well. tuples/frozensets may be immutable for instance, but the objects that it's part of may not be. Here's a small (incomplete) version that does a decent job of enforcing immutability all the way down:

# Initialize lists
a = [1,2,3]
b = [4,5,6]
c = [7,8,9]

l = [a,b]

# We can reassign in a list 
l[0] = c

# But not a tuple
t = (a,b)
#t[0] = c -> Throws exception
# But elements can be modified
t[0][1] = 4
t
([1, 4, 3], [4, 5, 6])
# Fix it back
t[0][1] = 2

li = ImmutableObject(l)
li
[[1, 2, 3], [4, 5, 6]]
# Can't assign
#li[0] = c will fail
# Can reference
li[0]
[1, 2, 3]
# But immutability conferred on returned object too
#li[0][1] = 4 will throw an exception

# Full solution should wrap all the comparison e.g. decorators.
# Also, you'd usually want to add a hash function, i didn't put
# an interface for that.

class ImmutableObject(object):
    def __init__(self, inobj):
        self._inited = False
        self._inobj = inobj
        self._inited = True

    def __repr__(self):
        return self._inobj.__repr__()

    def __str__(self):
        return self._inobj.__str__()

    def __getitem__(self, key):
        return ImmutableObject(self._inobj.__getitem__(key))

    def __iter__(self):
        return self._inobj.__iter__()

    def __setitem__(self, key, value):
        raise AttributeError, 'Object is read-only'

    def __getattr__(self, key):
        x = getattr(self._inobj, key)
        if callable(x):
              return x
        else:
              return ImmutableObject(x)

    def __hash__(self):
        return self._inobj.__hash__()

    def __eq__(self, second):
        return self._inobj.__eq__(second)

    def __setattr__(self, attr, value):
        if attr not in  ['_inobj', '_inited'] and self._inited == True:
            raise AttributeError, 'Object is read-only'
        object.__setattr__(self, attr, value)
Aliaalias answered 12/5, 2016 at 20:15 Comment(0)
K
1

You can just override setAttr in the final statement of init. THen you can construct but not change. Obviously you can still override by usint object.setAttr but in practice most languages have some form of reflection so immutablility is always a leaky abstraction. Immutability is more about preventing clients from accidentally violating the contract of an object. I use:

=============================

The original solution offered was incorrect, this was updated based on the comments using the solution from here

The original solution is wrong in an interesting way, so it is included at the bottom.

===============================

class ImmutablePair(object):

    __initialised = False # a class level variable that should always stay false.
    def __init__(self, a, b):
        try :
            self.a = a
            self.b = b
        finally:
            self.__initialised = True #an instance level variable

    def __setattr__(self, key, value):
        if self.__initialised:
            self._raise_error()
        else :
            super(ImmutablePair, self).__setattr__(key, value)

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")

if __name__ == "__main__":

    immutable_object = ImmutablePair(1,2)

    print immutable_object.a
    print immutable_object.b

    try :
        immutable_object.a = 3
    except Exception as e:
        print e

    print immutable_object.a
    print immutable_object.b

Output :

1
2
Attempted To Modify Immutable Object
1
2

======================================

Original Implementation:

It was pointed out in the comments, correctly, that this does not in fact work, as it prevents the creation of more than one object as you are overriding the class setattr method, which means a second cannot be created as self.a = will fail on the second initialisation.

class ImmutablePair(object):

    def __init__(self, a, b):
        self.a = a
        self.b = b
        ImmutablePair.__setattr__ = self._raise_error

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")
Kistler answered 23/3, 2017 at 11:20 Comment(2)
That won't work: you're overriding the method on the class, so you'll get NotImplementedError as soon as you try to create a second instance.Brantbrantford
If you want to pursue this approach, note that it's difficult to override special methods at runtime: see https://mcmap.net/q/111372/-overriding-__setattr__-at-runtime for a couple workarounds to this.Brantbrantford
T
1

Short Answer

Use pandatic's BaseModel with overriding Config:

from pydantic import BaseModel

class Point(BaseModel):
    x: float
    y: float

    class Config:
        allow_mutation = False

p = Point(x=3.14, y=2.72)

p.x = 0  # this operation raise TypeError, because the object is immutable

Long OOP-based Answer

Step 1: Set abstraction

Use pyndatic-package for implementation of reusable ImmutableModel:

from abc import ABC
from pydantic import BaseModel


class ImmutableModel(BaseModel, ABC):
    """Base immutable model."""

    class Config:
        allow_mutation = False

Step 2: Declare immutable structures

Declare Point and Vector classes:

class Point(ImmutableModel):
    """Immutable point."""

    x: float
    y: float
    z: float

class Vector(ImmutableModel):
    """Immutable vector."""

    start: Point
    end: Point

Step 3: Test results

# Test Point immutability ----
p = Point(x=3.14, y=2.72, z=0)

assert p.x == 3.14 and p.y == 2.72 and p.z == 0

try:
    p.x = 0  # try to change X value
except TypeError as e:  # error when trying to modify value
    print(e)
finally:
    assert p.x == 3.14  # X value wasn't modified

print(p)


# Test Vector immutability ----
v = Vector(start=Point(x=0, y=0, z=0), end=Point(x=1, y=1, z=1))

assert v.start != p and v.end != p

try:
    v.start = p
except TypeError as e: # error when trying to modify value
    print(e)
finally:
    assert v.start != p  # start point wasn't modified

print(v)
Triplicity answered 7/7, 2023 at 14:59 Comment(0)
T
0

I've created a small class decorator decorator to make class immutable (except inside __init__). As part of https://github.com/google/etils.

from etils import epy


@epy.frozen
class A:

  def __init__(self):
    self.x = 123  # Inside `__init__`, attribute can be assigned

a = A()
a.x = 456  # AttributeError

This support inheritance too.

Implementation:

_Cls = TypeVar('_Cls')


def frozen(cls: _Cls) -> _Cls:
  """Class decorator which prevent mutating attributes after `__init__`."""
  if not isinstance(cls, type):
    raise TypeError(f'{cls.__name__} is not a class.')

  cls.__init__ = _wrap_init(cls.__init__)
  cls.__setattr__ = _wrap_setattr(cls.__setattr__)
  return cls


def _wrap_init(init_fn):
  """`__init__` wrapper."""

  @functools.wraps(init_fn)
  def new_init(self, *args, **kwargs):
    if hasattr(self, '_epy_is_init_done'):
      # `_epy_is_init_done` already created, so it means we're
      # a `super().__init__` call.
      return init_fn(self, *args, **kwargs)
    object.__setattr__(self, '_epy_is_init_done', False)
    init_fn(self, *args, **kwargs)
    object.__setattr__(self, '_epy_is_init_done', True)

  return new_init

def _wrap_setattr(setattr_fn):
  """`__setattr__` wrapper."""

  @functools.wraps(setattr_fn)
  def new_setattr(self, name, value):
    if not hasattr(self, '_epy_is_init_done'):
      raise ValueError(
          'Child of `@epy.frozen` class should be `@epy.frozen` too. (Error'
          f' raised by {type(self)})'
      )
    if not self._epy_is_init_done:  # pylint: disable=protected-access
      return setattr_fn(self, name, value)
    else:
      raise AttributeError(
          f'Cannot assign {name!r} in `@epy.frozen` class {type(self)}'
      )

  return new_setattr
Trimeter answered 17/10, 2022 at 17:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.