249

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.

naught101
  • 18,687
  • 19
  • 90
  • 138
Lennart Regebro
  • 167,292
  • 41
  • 224
  • 251
  • 3
    Doesn't your code facilitate access to the attributes via `.a` and `.b`? That's what the properties seems to exist for after all. – Sven Marnach Jan 28 '11 at 12:33
  • 2
    @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? :-) – Lennart Regebro Jan 28 '11 at 12:40
  • @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. – Sven Marnach Jan 28 '11 at 13:42
  • 2
    Just another note: [`NotImplemented`](http://docs.python.org/library/constants.html#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. – Sven Marnach Jan 28 '11 at 16:01
  • 1
    @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. – Lennart Regebro Jan 28 '11 at 19:36
  • 2
    @Lennart: You could raise `NotImplementedError`, but `TypeError` is what a tuple raises if you try to modify it. – Sven Marnach Jan 28 '11 at 20:29
  • 1
    "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 : http://stackoverflow.com/questions/4859129/python-and-python-c-api-new-versus-init – Jérôme Radix Feb 02 '11 at 16:30
  • 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. :) – Lennart Regebro Feb 02 '11 at 16:34
  • To get rid of the access through `[0]` and `[1]`, can't you just override `__getitem__()` and make it raise an error? – PieterNuyts Apr 11 '19 at 08:31
  • 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. – Sam Hughes Jun 11 '21 at 13:34

27 Answers27

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.

Community
  • 1
  • 1
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 7
    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. – jfs Feb 01 '11 at 04:52
  • 2
    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! – Lennart Regebro Feb 02 '11 at 12:39
  • 2
    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 `namedtuple`s be copied by value when passed through functions? – hlin117 Oct 11 '15 at 17:46
  • 5
    @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. – Sven Marnach Oct 11 '15 at 21:03
  • 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. – Asaf Oct 13 '17 at 21:57
  • 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. – user48956 Feb 01 '18 at 04:55
  • 1
    @user48956 You can derive from the type – just make sure to add `__slots__` to the derived type so it does not inadvertently become mutable. – Sven Marnach Feb 01 '18 at 16:02
  • 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. – Alexey Feb 25 '18 at 10:46
  • @Alexey 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. – Sven Marnach Feb 25 '18 at 14:11
  • @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). – Alexey Feb 25 '18 at 15:46
  • @Alexey 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. – Sven Marnach Feb 25 '18 at 21:53
  • @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". – Alexey Feb 25 '18 at 21:57
  • @Alexey 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. – Sven Marnach Feb 25 '18 at 22:27
  • 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`). – dawid Feb 28 '21 at 19:17
  • @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. – Sven Marnach Mar 01 '21 at 10:07
103

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.

Jundiaius
  • 6,214
  • 3
  • 30
  • 43
  • 4
    Why was not the chosen solution? 2021 still working – Agapito Gallart Bernat Jan 12 '21 at 16:18
  • 7
    Because it doesn't allow a user-defined \_\_init\_\_ function to set attributes. The ideas of dataclass (plain old data) and immutability are orthogonal. – Brent Feb 11 '21 at 19:29
  • 3
    @Brent sure, you can have a custom init function with data classes. https://www.python.org/dev/peps/pep-0557/#custom-init-method – Balthazar Aug 09 '21 at 10:16
  • 3
    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." https://docs.python.org/3/library/dataclasses.html – Balthazar Aug 09 '21 at 10:33
  • 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://stackoverflow.com/a/54119384/885922 – xlm May 11 '22 at 14:19
  • @Brent A quick google on managing a custom `__init__` or `__post_init__` with a frozen dataclass gives lots of possibilities. – Simply Beautiful Art Jul 01 '22 at 06:18
96

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.

jfs
  • 399,953
  • 195
  • 994
  • 1,670
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 2
    Wouldn't it be a better idea to use a `tuple` here, `__slots__ = ()`, rather than `__slots__ = []`? (Just clarifying) – user225312 Jan 28 '11 at 12:20
  • 1
    @sukhbir: I think this does not matter at all. Why would you prefer a tuple? – Sven Marnach Jan 28 '11 at 12:24
  • 1
    @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? – user225312 Jan 28 '11 at 12:26
  • 7
    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. – Lennart Regebro Jan 28 '11 at 12:36
  • But your solution is better than overriding `__setattr__` so it's an improvement over mine. +1 :) – Lennart Regebro Jan 28 '11 at 12:51
  • What is the advantage compared to `Point = namedtuple("Point", "x y")`? – jfs Jan 31 '11 at 17:53
  • @J.F. Sebastian: Full control over the code, and it works in Python 2.5. – Sven Marnach Jan 31 '11 at 18:08
  • 1
    @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). – Reb.Cabin Mar 17 '17 at 22:31
  • @jfs One advantage of this, over `namedtuple`, is that you can specify default values in the `__new__` signature – James Schinner Oct 29 '17 at 23:05
  • @JamesSchinner there is `typing.NamedTuple` which allows to specify default values. – jfs Oct 29 '17 at 23:12
61

..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")
Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • @Lennart: Instances of `namedtuple` (or more precisely of the type returned by the function `namedtuple()`) are immutable. Definitely. – Sven Marnach Jan 31 '11 at 19:29
  • @Lennart Regebro: `namedtuple` passes all the tests (except indexing support). What requirement did I miss? – jfs Jan 31 '11 at 19:31
  • Yes, you are right, I made a namedtuple type, instantiated it, and then did the test on the type instead of the instance. Heh. :-) – Lennart Regebro Jan 31 '11 at 21:51
  • may I ask why would one needs weak referencing here? – McSinyx Mar 21 '20 at 14:51
  • 1
    @McSinyx: otherwise, the objects can't be used in weakref's collections. [What exactly is `__weakref__` in Python?](https://stackoverflow.com/questions/36787603/what-exactly-is-weakref-in-python) – jfs Mar 22 '20 at 04:52
  • The immutables library is a native C library with Python bindings for this behavior. See https://github.com/MagicStack/immutables – David Golembiowski Jul 13 '21 at 17:15
45

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.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 17
    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. – zvone Feb 04 '12 at 21:54
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
PaoloVictor
  • 1,296
  • 8
  • 18
  • 1
    Making a (class?) decorator or metaclass out of the solution is indeed a good idea, but the question is what the solution is. :) – Lennart Regebro Jan 28 '11 at 12:52
  • 3
    `object.__setattr__()` breaks it http://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python/4829374#4829374 – jfs Jan 31 '11 at 17:48
  • Indeed. I just carried on as an exercise on decorators. – PaoloVictor Jan 31 '11 at 20:01
11

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)
>>>      
Toothpick Anemone
  • 4,290
  • 2
  • 20
  • 42
Duncan
  • 92,073
  • 11
  • 122
  • 156
  • 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. – Lennart Regebro Feb 02 '11 at 12:37
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.

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • `object.__setattr__()` breaks it http://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python/4829374#4829374 – jfs Jan 31 '11 at 17:47
  • 3
    Using stack inspection to ensure the caller is `__init__` is not very satisfying. – gb. Jul 26 '13 at 07:02
  • 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. – Michael Scott Asato Cuthbert Oct 11 '22 at 10:51
  • @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. – Michael Scott Asato Cuthbert Oct 11 '22 at 11:05
  • 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. – Michael Scott Asato Cuthbert Oct 11 '22 at 11:49
  • 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. – Michael Scott Asato Cuthbert Oct 11 '22 at 11:54
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.

Alexander Ryzhov
  • 2,705
  • 3
  • 19
  • 19
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.

Community
  • 1
  • 1
Bernhard
  • 2,084
  • 2
  • 20
  • 33
  • `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](http://stackoverflow.com/a/4854045/4279) e.g., `instance.b = 1` creates a new `b` attribute. – jfs Jul 02 '15 at 12:29
  • Right, the question is really how to prevent doing `A().b = "foo"` ie not allowing setting new attributes. – Lennart Regebro Jul 02 '15 at 13:36
  • 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. – Bernhard Jul 02 '15 at 13:44
  • @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. – Bernhard Jul 02 '15 at 13:49
  • @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. – Bernhard Jul 02 '15 at 15:11
  • Yeah, slots + properties is good enough for most usecases, even if it's not truly immutable. – Lennart Regebro Jul 02 '15 at 16:02
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/

David
  • 816
  • 1
  • 6
  • 16
RaghuGolla
  • 51
  • 1
  • 2
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.

rob
  • 36,896
  • 2
  • 55
  • 65
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
Community
  • 1
  • 1
  • 4
    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. – C S Oct 01 '18 at 03:22
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
rayepps
  • 2,072
  • 1
  • 12
  • 22
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.

Ale
  • 1,998
  • 19
  • 31
dangirsh
  • 301
  • 1
  • 4
  • 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. http://stackoverflow.com/a/22545808/5987 – Mark Ransom Mar 20 '14 at 22:44
  • 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)` – Pete Cacioppi May 07 '15 at 19:53
  • 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". – Pete Cacioppi May 07 '15 at 20:01
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__')
Alex Coventry
  • 68,681
  • 4
  • 36
  • 40
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__.

cmc
  • 973
  • 8
  • 17
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
  • Or just `pass` instead of `raise NotImplementedError` – Jonathan Scholbach May 15 '19 at 11:38
  • 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. – darlove Dec 19 '19 at 21:43
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.

jjmerelo
  • 22,578
  • 8
  • 40
  • 86
theengineear
  • 857
  • 6
  • 6
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()
ipap
  • 301
  • 2
  • 11
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
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.

Mark Horvath
  • 1,136
  • 1
  • 9
  • 24
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]
Michael Amrhein
  • 206
  • 2
  • 6
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)
Corley Brigman
  • 11,633
  • 5
  • 33
  • 40
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")
phil_20686
  • 4,000
  • 21
  • 38
  • 1
    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. – slinkp Nov 28 '17 at 23:16
  • 1
    If you want to pursue this approach, note that it's difficult to override special methods at runtime: see https://stackoverflow.com/a/16426447/137635 for a couple workarounds to this. – slinkp Nov 28 '17 at 23:17
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)
codez0mb1e
  • 706
  • 6
  • 17
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
Conchylicultor
  • 4,631
  • 2
  • 37
  • 40