How to make an immutable object in Python?
Rise to the top 3% as a developer or hire one of them at Toptal: https://topt.al/25cXVn
--------------------------------------------------
Music by Eric Matyas
https://www.soundimage.org
Track title: Techno Intrigue Looping
--
Chapters
00:00 How To Make An Immutable Object In Python?
00:51 Answer 1 Score 133
01:27 Accepted Answer Score 120
02:41 Answer 3 Score 96
04:08 Answer 4 Score 64
04:47 Answer 5 Score 46
05:37 Thank you
--
Full question
https://stackoverflow.com/questions/4828...
--
Content licensed under CC BY-SA
https://meta.stackexchange.com/help/lice...
--
Tags
#python #python3x #immutability
#avk47
ANSWER 1
Score 133
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.
ANSWER 2
Score 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.
ANSWER 3
Score 64
..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")
ANSWER 4
Score 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.