Adding an Abstract Base Class for Immutable types

Suppose I want to have some of my classes be immutable, there is no simple way to do this in python. I can perhaps do something like this:

class MyImmutable:
    def __setattr__(self, key, value):
        raise TypeError

or perhaps I can inherit from an immutable type like so

class MyImmutable(namedtuple('Immutable', 'foo bar')):
    def __setitem__(self, item, value):
        raise TypeError
    ...

but I would have to do this for every immutable class, which becomes complicated. It would be nicer if there was something like an abc.Immutable abstract class specifically for creating immutable classes. Then the any class that I want to be strictly immutable can be defined as:

class MyImmutable(abc.Immutable):
    ...

with no need to override all the setter methods. This would also allow for optimizations of immutable subclasses in memory and performance similar to builtin immutable types.

1 Like

There are more cases to cover, if Foo = namedtuple("Foo", "bar baz"), Foo("immutable", ["mutable", "list"]) can be mutated without __setattr__, modifying in-place the baz list.

We should restrict the attributes types to immutable ones, or maybe, just during the type checking, disable the mutating methods of exposed types. In the previous example, inhibit baz.append(), baz.insert(), but also index and slice assigns.

Not easy, but I’d like a check of this kind, I looked behind a bug because the code was mutating a cached item once, and I found no way to enforce the behavior without a performance penalty (convert the list to a tuple or stuff like that).

I agree that this is another case/consideration, although I think raises a more general point about what immutable means in python. I would expect an immutable type to behave the same as a tuple. Perhaps this points to needing 2 abstract classes, Immutable and Frozen, where a frozen subclass behaves more like a frozenset i.e. everything must be immutable all the way down as you point out?

Why can’t you use a frozen dataclass?

This is similar to overriding setter methods or inheriting from namedtuple. All three of which work but become messy when you do so in lots of places. Especially since, correct me if I am wrong here, for all three there are issues if you have a more complex instancing method than simple assignment. e.g. when using @dataclass(frozen=True) I cannot have

@dataclass(frozen=True)
class MyImmutable:
    def __init__(self, x, y):
        self.z = x+y

Perhaps the original topic post is not complete in that it doesn’t point out this limitation for the others either.

Presumably you can write your own equivalent of abc.Immutable, so instead of

import abc

class MyImmutable(abc.Immutable):
    ...

you’d do

import app_abc

class MyImmutable(app_abc.Immutable):
    ...

Given that there’s no real consensus on what a standard library Immutable ABC would do, I’d have assumed that a locally-defined version that did what you consider important would be easy enough to maintain and not a huge overhead for code that uses it.

I think a real win is to have mypy know what Immutable means, with no runtime overhead if unwanted.

Fair point, I don’t know enough about mypy to comment on that. Although I’d imagine that “it depends what you mean by immutable” would be just as much of an issue in that case…

What other means of Immutable are you thinking? Transitive or non transitive on nested structs? Or something else?

There’s been discussions here about whether updating lists that are attributes of the main object should be allowed. And there’s things like “Can an immutable object have a non-immutable attribute”? And this topic comes up fairly regularly on the python-ideas mailing list.

My point is that a stdlib class has to work through all of these and come to a consensus (which probably won’t please everyone). A custom local ABC can define things however the project wants. And when I said “I don’t know enough about mypy”, I meant that I don’t know why mypy might find a stdlib class any easier to deal with than a project-local one.

To me this would suggest making a clearer distinction between the two. If both are expected behaviour depending on the container type and use case then neither is wrong, but using the same terminology for both is wrong.
There is some amount of separation already in the use of frozen and immutable e.g. frozenset is a frozen set and a tuple is an immutable list. Although one, I suppose, could call a tuple a frozen list and the fact that a frozenset is “fully” immutable is thanks to hashability.

My point being that embracing a separation of the terms to be related but strictly different could appease both sides of that argument. And in the case of an abc.Immutable class, would mean there ought to be also an abc.Frozen (or something).

You want frozen=True init=False and you have to declare z, i.e.:

@dataclass(frozen=True, init=False)
class MyImmutable:
z: int
def init(self, x, y):
self.z = x+y

Apologies for any typos, replying on phone.

– Howard.

I think that having an Immutable ABC would go some way to reaching a consensus, rather than each person having their own special ABC that duck types to immutable for their own use but not for others.

Also having my own method would not have the potential performance benefits, I see that this isn’t a big win really but it is something worth keeping in mind.

This does not seem to work for me…


@dataclass(frozen=True)
class AbstractImmutable(ABC):
    z: int  # Must have at least one field for frozen to work!
        
    @abstractmethod # Must have at least one abstract method for ABC to work!
    def m(self):
        pass

@dataclass(frozen=True, init=False)
class Immutable(AbstractImmutable):
    def __init__(self, x, y):
        super().__init__(x+y)
        
    def m(self):
        print(self.z)

Immutable can’t be a base class for type-checking purposes, because the subtyping semantics are backwards: any operation that works on an immutable type will also work on an mutable version of that type, but not vice-versa. Therefore, according to the Liskov substitution principle, subtypes of Immutable can be mutable. Therefore, isinstance(x, Immutable) doesn’t actually mean that x is immutable.

And similarly, ABCs are all about organizing types into categories and doing isinstance checks, so Immutable doesn’t make sense as an ABC. In fact, instances of the object type are immutable, therefore object implements the Immutable contract, therefore if we did have an Immutable ABC then literally every type in python would implement it… which doesn’t make sense.

It might make sense to have an immutability-adding-mixin, as a convenient way to add some custom __setattr__… though I’d probably use the attrs/dataclass features for defining immutable types, or some other kind of class decorator.

1 Like

@njs whilst what you say is true type wise, it isn’t true for dataclasses. However; you have to stick to dataclasses, because it is easy to circumvent with a non-dataclass :confused:

This adds a lot of code around defining an immutable class, so although it does what I would need it is less versatile and more complex that I would like. It would be ideal if dataclass(frozen=True, init=False) did not freeze the class until after instantiation, or if there was an equivalent decorator that did the same, the key being after creation so that complex __init__ methods do not need a lot of additional complex code, obscuring their intent.

I can agree with what you are saying about type checking, however is it still the case if subclass of Immutable were forbidden from overriding the methods to make them mutable? Which would make an subclass of Immutable always immutable.

I can see that that could be not to everyone’s taste so, perhaps instead the same thing would be achievable indirectly and perhaps with more use cases if instead of a base class there was an decorator for the opposite of @abstractmethod, i.e. something that could not be overridden by a subclass.
This is obviously a different idea to a base class or a way to have immutable instances, but it might allow for safer abstract class definitions.