Immutable classes

Hi.

Suggestion

There are many situations where it would be helpful to annotate a class as representing an immutable object, meaning all its properties may not be modified after the __init__() method completes.

Examples:

  1. many builtins are immutable, like str or tuple
  2. frozen dataclasses
  3. Go from (Type qualifiers — typing documentation)
class ImmutablePoint:
    x: Final[int]
    y: Final[int]  # Error: final attribute without an initializer

to

@Immutable
class ImmutablePoint:
    x: int
    y: int

There is already @final for classes but that means it cannot be subclassed.
I used @Immutable in the example, but other names could be used like @Frozen.

Use case

The use case is to clearly show to type checkers that the properties of an object may not be assigned, without having to annotate all individual members, e.g.

p = ImmutablePoint(1, 2)
p.x = 1 # Error

In contexts where mutable objects are not recommended, tagging as immutable silences the type checker, e.g.

def my_fn(p: ImmutablePoint = ImmutablePoint (0, 0)):
    # ^ currently type checkers warn about not using mutable objects as function defaults
    # but if it is a frozen/immutable object, it is fine and there should not be a warning.

Semantics

Of course, tagging a class as Immutable does not imply anything regarding the immutability of its members.

The semantics of @Immutable requires that neither properties (get/setattr) nor keys (get/setitem) may be modified (added, assigned, deleted); that any methods that might change the state of the object must return a copy, like newobj = dataclasses.assign(obj, **kwds).

But obviously, these are static type hints and no runtime checks would be performed. It would be up to the developer to decide how much runtime checking he/she needs to implement in the class.

Inheritance

Immutability would not be inherited, e.g.

class MutablePoint(ImmutablePoint):
   ...

here MutablePoint is not decorated so it can be mutated. Immutability is only applicable to the class being directly decorated, not to its ancestors and not its descendants. This is also why I prefered the @decorator syntax instead of a mix-in class.
And this is perhaps the main difference from member: Final[type] which sticks to properties even with inheritance. If it is desirable for a whole hierarchy of classes to be immutable, the whole chain of classes needs to be decorated.

1 Like

What type checker(s) are you referring to?

For the use case you’ve put forward, immutability can be achieved (I think) with named tuple:

from typing import NamedTuple


class ImmutablePoint(NamedTuple):
    x: int
    y: int

While the example you gave might be a simplified one, I wanted to ask whether this would be enough for you or are you missing something?

1 Like

ReadOnly is currently restricted to be used only in TypedDict, but there has been some discussion for extending it to areas like this.

class ImmutablePoint:
    x: ReadOnly[int]
    y: ReadOnly[int]

This would mean (and I think it should mean) that x and y are only allowed to be defined in __init__ or __new__ (or to set the default instance value in the class scope).

We just need someone to do the work of specifying it.

In order to be type-safe, it can only be immutable/readonly if the ancestors are immutable/readonly.

example:

class A:
    x: int = 3


class B(A):
    x: ReadOnly[bool] = random.choice((False, True))


def foo(a: A) -> None:
    a.x = 4


b = B()
foo(b)
if b.x:
    assert b.x is True
    print("It's True")
else:
    reveal_type(b.x)  # Literal[False]
    assert not b.x,  "Crash!"

I don’t really see any future for this which is beneficial that doesn’t involve also adding runtime semantics. The value of immutability comes from what you can do when it’s actually guaranteed, and python’s type system doesn’t promise the soundness required to leverage this in this way.

I’d suggest looking into immutable structures available both in the standard library and in 3rd party for any uses you have for immutability, rather than relying on the type system to enforce this.

Some examples in the standard library: tuple, frozenset, namedtuple, frozen dataclasses.
Some examples of 3rd party libraries providing stronger immutability: immutables, msgspec

typing can accurately describe these, but typing can’t enforce immutability on arbitrary fields of python objects.

Another benefit I did not mention is that immutable objects are safe to share between threads and don’t need internal synchronization.

Replies

What type checker(s) are you referring to?

There is a flake8 check that reports this (mutable argument defaults)

While the example you gave might be a simplified one

It is just a simplified example yes. Not everything fits into a NamedTuple.

I wanted to ask whether this would be enough for you

I’m not trying to solve some problem I have. I’m suggesting a feature for the typing module.

In order to be type-safe, it can only be immutable/readonly if the ancestors are immutable/readonly.

Right, because of up-casting-

I don’t really see any future for this which is beneficial that doesn’t involve also adding runtime semantics.

This suggestion is for typing, not collections.abc. For collections.abc, yes, there would be some runtime implementation. For typing, no, since rules form the typing module are enforced by type checkers and not by the python VM. This suggestion is for the typing module.

The value of immutability comes from what you can do when it’s actually guaranteed, and python’s type system doesn’t promise the soundness required to leverage this in this way.

Well, I’m not suggesting to add any feature to the language to make objects immutable. This is a suggestion for the typing module, and therefore the developer as has the responsibility of implementing the class properly which does not mutate its state. Also, the type checker (e.g. mypy) would warn if the implementation of the class had methods that mutate its state.

typing can’t enforce immutability on arbitrary fields of python objects.

And again that is not the suggestion here.

I agree that an immutability marker will be useful. If we are going to formally bring the concept into part of the specification, I think we also need some consideration on the envisioned programming model.

Indeed immutability would be most helpful in conjunction with free-threading and functional programming. Right now immutability in Python has the status of a gentleman’s agreement between consenting adults: it is usually written in the documentation of libraries warning the users not to do nasty stuff to them, but it’s essentially undefined behavior if one tries to call {set,del}{attr,item} on it (does the {set,del}{attr,item} attribute exist? Is it None? Does calling it raise NotImplementedError or AttributeError or TypeError? Or does it just silently does nothing? Or is it actually mutable?)

dataclass

Frozen dataclasses raise a subclass of AttributeError, and I think this is the best approach in terms of runtime behavior, but can be very hard to introspect, especially in the case of non-dataclasses.

Some of these conditions can probably be detected automatically, some cannot, and a marker for immutability will be useful if not just to aid coding and introspection.

The immutability marker as it is currently proposed can serve as a first step to gauge usage, and in my opinion this (along with Final) is one the few proposed features that are worthy of exploring core syntax support in the future. Runtime guarantees on immutability will likely open up benefits in terms of optimization for free-threading / JIT.

Could you provide an example which doesn’t fit into a NamedTuple, and where your proposal would? I’m failing to see what gap this feature would fill. Thanks

And that puts too much burden on developers to get things right, hence this feature to help with code checking tools.

Mappings are not tuples, sets are not tuples, dataclasses are not tuples, immutable sequences might not have named properties, and generic classes with public and private properties are not tuples.
Also, this is not about language features (e.g. collections.abc). This is about the typing module and type checkers. A proposed feature for collections.abc could follow once one has a prototype or beta implementation on typing.

I don’t think this belongs in typing. The type system should describe python objects accurately. Unless runtime has immutability, the type system shouldn’t care.