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:
- many builtins are immutable, like
str
ortuple
- frozen dataclasses
- 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.