Monarch
(Monarch)
June 5, 2025, 5:54pm
1
Consider my stub file:
@final
class Meta:
title: str | None
passwords: tuple[str, ...]
tags: tuple[str, ...]
category: str | None
def __new__(
cls,
*,
title: str | None = None,
passwords: Sequence[str] = (),
tags: Sequence[str] = (),
category: str | None = None,
) -> Meta: ...
def __eq__(self, value: object) -> bool: ...
def __hash__(self) -> int: ...
The stub above represents the following Rust class:
#[pyclass(frozen, eq, hash)]
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Meta(foobar::Meta);
This class is frozen , so the following example code fails at runtime but passes mypy --strict
:
meta = Meta(title="hello")
assert meta.title == "hello"
meta.title = "world"
Traceback (most recent call last):
File "redacted", line 5, in <module>
meta.title = "world"
^^^^^^^^^^
AttributeError: attribute 'title' of 'Meta' objects is not writable
How do I tell the typecheckers that this is a frozen class?
1 Like
beauxq
(Doug Hoskisson)
June 5, 2025, 6:07pm
2
1 Like
Monarch
(Monarch)
June 5, 2025, 7:10pm
3
This seems like exactly what I need! Although I would love a way to mark the entire class frozen (similar to dataclass(frozen=True)
). Unfortunately it seems the PEP is still draft so I can’t use it right now.
tmk
(Thomas Kehrenberg)
June 6, 2025, 11:20am
4
A horrible hack, but it seems to work:
from collections.abc import Sequence
from typing import Any, Callable, TypeVar, dataclass_transform
_T = TypeVar("_T")
def field(
*,
default: Any,
converter: Callable[[Any], Any],
) -> Any: ...
@dataclass_transform(frozen_default=True, field_specifiers=(field,), eq_default=True)
def frozen_class(cls: type[_T]) -> type[_T]: ...
def sequence_to_tuples(s: Sequence[_T]) -> tuple[_T, ...]: ...
@frozen_class
class Meta:
title: str | None
passwords: tuple[str, ...] = field(converter=sequence_to_tuples, default=())
tags: tuple[str, ...] = field(converter=sequence_to_tuples, default=())
category: str | None = None
meta = Meta(title="hello")
assert meta.title == "hello"
meta.title = "world"
mypy:
main.py:26: error: Property "title" defined in "Meta" is read-only [misc]
pyright:
Cannot assign to attribute "title" for class "Meta"
Attribute "title" is read-only
Attribute "title" is read-only (reportAttributeAccessIssue)
You can use property
for this:
@property
def title(self) -> str | None: ...
If you don’t define a setter method then it is assumed to be read only.
2 Likes
peterc
(peter)
June 6, 2025, 12:30pm
6
I believe it is possible to use a dataclass as a type stub, so you could actually use
@final
@dataclass(frozen=True)
class Meta:
title: str | None = None
passwords: tuple[str, ...] = field(default_factory=tuple)
tags: tuple[str, ...] = field(default_factory=tuple)
category: str | None = None
in a stub file.
But I’m not a type stub expert by any means.
Eneg
(Eneg)
June 6, 2025, 7:47pm
7
Be aware that type checkers assume a bunch of stuff about dataclass-transformed classes, like the presence of __replace__
and __dataclass_fields__
Eneg
(Eneg)
June 6, 2025, 7:49pm
8
Since you’ve marked the class @final
you may very well use Final
on each field to denote the read-only aspect.
2 Likes
jorenham
(Joren Hammudoglu)
June 9, 2025, 4:24pm
9
Oscar Benjamin:
You can use property
for this:
@property
def title(self) -> str | None: ...
If you don’t define a setter method then it is assumed to be read only.
I don’t think that stubtest will like this.
jorenham
(Joren Hammudoglu)
June 9, 2025, 4:25pm
10
This is also what I would do.
1 Like
Monarch
(Monarch)
June 9, 2025, 10:06pm
11
Thank you everyone, typing.Final
will get the job done for now. I do wish we can get a way to mark the entire class frozen in future, something like a @typing.frozen
decorator.