20

I'm doing some distributed computing in which several machines communicate under the assumption that they all have identical versions of various classes. Thus, it seems to be good design to make these classes immutable; not in the sense that it must thwart a user with bad intentions, just immutable enough that it is never modified by accident.

How would I go about this? For example, how would I implement a metaclass that makes the class using it immutable after it's definition?

>>> class A(object):
...     __metaclass__ = ImmutableMetaclass
>>> A.something = SomethingElse # Don't want this
>>> a = A()
>>> a.something = Whatever # obviously, this is still perfectly fine.

Alternate methods is also fine, such as a decorator/function that takes a class and returns an immutable class.

porgarmingduod
  • 7,668
  • 10
  • 50
  • 83
  • @porgarmingduod: Please elaborate more what you actually mean by `immutable enough that it is never modified by accident`. Thanks – eat Feb 14 '11 at 20:20
  • @porgarmingduod: Yes, what kind of "accident" do you expect? – S.Lott Feb 14 '11 at 20:28
  • 1
    @eat: Since I am making a system where __instances__ of classes are magically distributed, a programmer may forget that the actual classes aren't. When variables are thrown around in the heat of some complex coding, I want an exception thrown if anyone ever tries to assign something to an immutable class. As for the `accident` part, all I meant is that the solution doesn't have to prevent a moron from trying to pry apart the internals of the solution. – porgarmingduod Feb 14 '11 at 20:44
  • 6
    possible duplicate of [How to make an immutable object in Python?](http://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python) – Ned Batchelder Feb 14 '11 at 20:48
  • "a programmer may forget that the actual classes aren't"? Really? There are no unit tests to detect this kind of abuse? – S.Lott Feb 14 '11 at 20:59
  • @porgarmingduod: No offense, but I don't know what you mean by `magically distributed` nor what needs to be done if `a programmer may forget that the actual classes ...`. Right now only what I can do is to advice you to `import this` and follow the guidance: `Explicit is better than implicit. Simple is better than complex.` Thanks – eat Feb 14 '11 at 21:03
  • @Ned: This question is less about perfect immutability and more about preventing careless access. And, the so called `duplicate` is at best be a hint in the right direction as far as I can tell. – porgarmingduod Feb 14 '11 at 21:04
  • 2
    @eat: The fact that your comment starts with "no offense" does not mitigate the fact that I find your `import this` answer offensive. – porgarmingduod Feb 14 '11 at 21:06
  • @porgarmingduod: Would you consider to elaborate more on your question body, so we (or at least me) more mortals are able to follow what you are aiming for? Really, no offense – eat Feb 14 '11 at 21:09
  • 5
    @eat: What is not to understand about the question? Is it unclear that I am asking about a way to make assigning to/deleting attributes from a class raise an exception? I understand that there may exist objections as to whether such functionality is the right solution, but that is not part of the question. I could write a lengthy defense as to why I think it is. Or I could fail. But the question remains **how to make an immutable class**. – porgarmingduod Feb 14 '11 at 21:18
  • https://github.com/lihaoyi/macropy#case-classes – Mauricio Scheffer Nov 27 '15 at 15:14

4 Answers4

10

If the old trick of using __slots__ does not fit you, this, or some variant of thereof can do: simply write the __setattr__ method of your metaclass to be your guard. In this example, I prevent new attributes of being assigned, but allow modification of existing ones:

def immutable_meta(name, bases, dct):
    class Meta(type):
        def __init__(cls, name, bases, dct):
            type.__setattr__(cls,"attr",set(dct.keys()))
            type.__init__(cls, name, bases, dct)

        def __setattr__(cls, attr, value):
            if attr not in cls.attr:
                raise AttributeError ("Cannot assign attributes to this class")
            return type.__setattr__(cls, attr, value)
    return Meta(name, bases, dct)


class A:
    __metaclass__ = immutable_meta
    b = "test"

a = A()
a.c = 10 # this works
A.c = 20 # raises valueError
Cristian Ciupitu
  • 20,270
  • 7
  • 50
  • 76
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Note that you can still do `type.__setattr__(A, 'c', 10)` and then `A.c` exists. This doesn't truly make it immutable, but in most cases it will be "good enough" immutability. – ely Oct 20 '14 at 02:35
  • The most correct work-around would be `object.__setattr__` - it is the setattr on the class itself, not of its type that matters. The `type.__setattr__` works due it being the same as object's. But indeed - in pure Python it is most likely impossible to avoid work arounds to any such restriction one can come up with - and it is on the "Pythonic" way of being that it should be that way. – jsbueno Oct 20 '14 at 16:15
  • 1
    Actually, you cannot call `object`'s `__setattr__` on a object that inherits from `type`. If you try `object.__setattr__(A, 'c', 10)` you get an error: "`TypeError: can't apply this __setattr__ to type object`". – ely Oct 20 '14 at 16:35
  • Indeed - I had not noticed that. – jsbueno Jul 15 '16 at 13:11
  • I copy, pasted. and ran this code, and it does not raise a valueError in Python 3.7. Adding print(A.c) at the bottom produces "20". – wolfblade87 Mar 01 '19 at 18:55
  • This code is for Python 2.7 - although it will likely work on Python 3 if you simply use the correct syntax to declare the Metaclass - that is: `class A(metaclss=immutable_meta)` – jsbueno Mar 01 '19 at 19:35
7

Don't waste time on immutable classes.

There are things you can do that are far, far simpler than messing around with trying to create an immutable object.

Here are five separate techniques. You can pick and choose from among them. Any one will work. Some combinations will work, also.

  1. Documentation. Actually, they won't forget this. Give them credit.

  2. Unit test. Mock your application objects with a simple mock that handles __setattr__ as an exception. Any change to the state of the object is a fail in the unit test. It's easy and doesn't require any elaborate programming.

  3. Override __setattr__ to raise an exception on every attempted write.

  4. collections.namedtuple. They're immutable out of the box.

  5. collections.Mapping. It's immutable, but you do need to implement a few methods to make it work.

Cristian Ciupitu
  • 20,270
  • 7
  • 50
  • 76
S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • 1
    @S.Lott: Surely there are more solid ways, such as overriding the dictproxy used by classes? But I haven't done such things before, hence the question. An additional note: I think you could have spared the comments on why not to do this, as that aspect is surely given enough attention in the comments for the original question. – porgarmingduod Feb 14 '11 at 21:56
  • 2
    @porgarmingduod: This is an answer, not more comments. It stands alone. Other people with the same question aren't going to thread through the comments. There is no "more solid way". It's a waste of brain calories. Programmers aren't stupid, forgetful, accident-prone, or sociopathic. Build your app. Get it to work. Raise exceptions. That's the Pythonic way to do it. – S.Lott Feb 14 '11 at 21:58
  • 8
    @S.Lott: Well, I guess you are allowed to interpret my question as "How can I make an immutable class, or, alternatively, can anyone tell me why I shouldn't?". Maybe I will have more luck with "How can I make an immutable class if we take for granted that I actually have a good reason to do this." I may not get any answers, but I'd take that any day over someone telling me I do not know what I want, repeatedly, even after I explicitly ask for an answer to the actual question on hand. – porgarmingduod Feb 14 '11 at 22:12
  • "take for granted that I actually have a good reason to do this". Sorry. Can't take it for granted. It's a difficult problem in Python and -- what's important -- it has no value. – S.Lott Feb 14 '11 at 22:14
  • @porgarmingduod: You are free to demand that we all solve a very, very bad idea for you. However, what you'll notice is that we aren't interested in pursing a very, very bad idea. There's a reason why we won't. You can repeatedly insist that it's absolutely necessary, and all we can do is remind you that it's so rarely used in Python that there isn't a standard recipe. No framework relies on it. We're all very, very happy without it. – S.Lott Feb 14 '11 at 22:15
  • 2
    @S.Lott: Where did I demand you do anything for me? Implying I am demanding anything from you is pretty irrational; you cannot possibly find any basis to make that claim in anything I have said. And I have long since conceded that I what I am trying to do may not be a good idea. I am not even trying to defend that point. I am merely tryint to defend the actual question, which is very specific and can be answered. You made your point about "this may be a stupid idea" in the initial comments. Also, kudos for referring to your own opinions as "we". Are you the python community consensus? – porgarmingduod Feb 14 '11 at 22:23
  • @porgarmingduod: "this may be a stupid idea" Where was that? I missed it. "Where did I demand you do anything for me?"? I don't get that, either. The comments sound like a demand for very, very specific advice. I guess I was wrong. It appeared that more than one person don't want to give the required answer. I thought that I could include the other commenters in "we". I apologize for including anyone else in my comments. I apologize for only providing 5 alternative solutions to make a class raise exceptions on an attempt to update. – S.Lott Feb 15 '11 at 01:18
  • 4
    It can be very instructive for learning about Python's data model to practice with immutable classes. Understanding why `object.__setattr__` or `type.__setattr__` can still bypass most home brewed attempts is a good learning experience. It's a shame that you choose the approach of hearing a question where someone says "I want to do X" and you just reply "Stop wanting to do X". Wanting to do X can be very useful, if only just for pedagogy, and the answer space is not a good location for your personal normative judgement of the question's premise. Make a comment. Or, better, just ignore it. – ely Oct 20 '14 at 02:37
  • Why is this still the top answer when the next answer down actually answers the question, has more upvotes, and is the accepted answer? – linkhyrule5 Aug 20 '23 at 04:40
1

If you don't mind reusing someone else's work:

http://packages.python.org/pysistence/

Immutable persistent (in the functional, not write to desk sense) data structures.

Even if you don't use them as is, the source code should provide some inspiration. Their expando class, for example, takes an object in it's constructor and returns an immutable version of it.

mavnn
  • 9,101
  • 4
  • 34
  • 52
  • 1
    Unfortunately, the `expando` class isn't immutable. I made the same mistake reading the documentation, but the package only supplies immutable list and dict. Reading the source, I quickly realized the `expando` class is simply a class that allows you to make copies of a class with changes - it does not however __prevent__ changes. – porgarmingduod Feb 14 '11 at 20:47
  • Good spot, I had missed that. – mavnn Feb 15 '11 at 12:25
1

The main approaches for archive immutability:

  1. use immutable structures, e.g. tuple, frozenset,
  2. use @dataclass(frozen=True) decorator,
  3. use pyndatic's BaseModel with Config overriding:
from pydantic import BaseModel

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

    class Config:
        allow_mutation = False

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

p.x = 0  # this operation raise TypeError, because the object is immutable
codez0mb1e
  • 706
  • 6
  • 17