-
Notifications
You must be signed in to change notification settings - Fork 288
-
I've read PEP 526 and searched around, but can't find any mention of why generics are not allowed in a ClassVar (just that they're not allowed). I was hoping to write a class like this:
@dataclass class Foo(Generic[T]): Type: ClassVar[T]
EDIT: I think I may have a misunderstanding of how to do what I want. I want to make a subclass like this: class Bar(Foo[int]): ... and use Bar.Type as a type annotation later on. Looks like this may not be possible?
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 3
Replies: 6 comments 2 replies
-
I can't think of any technical reason why this isn't allowed.
Perhaps it's because class variable accesses typically don't include explicit class specialization. Using your example above, Foo[int].Type would be less typical than Foo.Type. The latter would evaluate to T or Any, depending on whether the type checker replaces the unsolved TypeVar with its default value.
FWIW, pyright emits an error if you attempt to use a type variable within a ClassVar definition (consistent with the text in PEP 526), but it otherwise works as you would expect. Mypy emits errors for both the definition and the subsequent use of the ClassVar field.
Beta Was this translation helpful? Give feedback.
All reactions
-
I forget that this limitation exists because it makes no sense WRT my mind-abstraction of Generics. I think it is the 2nd time I end up hitting this wall in two entirely separate projects 😆
Please provide solid reasoning or add the feature.
Beta Was this translation helpful? Give feedback.
All reactions
-
I'd like to throw my hat into the ring here, I'm not sure why this limitation exists, it seems inconsistent with the purpose of typings. Take for example this case where some abstract View class requires an input and return class to be declared by the implementors
class View(ABC, Generic[Message, Result]): # Limitation of the pep https://peps.python.org/pep-0526/#class-and-instance-variable-annotations # see https://github.com/python/mypy/issues/5144#issuecomment-1001222212 # and https://github.com/python/typing/discussions/1424 accepts: ClassVar[Message] # type: ignore[misc] returns: ClassVar[Result] # type: ignore[misc]
The expected utilization might be something like
class MyResult: def __init__(self, result: list[dict]): self.result = result class MyView(View): returns = MyResult def serialize(self, result: list[dict]) -> MyResult: return MyView.returns(result)
I understand that if accepts or returns is not initialized, things will break, was this the original intention behind the limitation? If not, what was?
What steps would we need to take to have this limitation removed? It seems like a library such as mypy or pylint could handle the warning(s) related to uninitialized class variables but I don't think they're able to move forward until the restriction here is removed.
Beta Was this translation helpful? Give feedback.
All reactions
-
I believe the technical reason why this was not allowed is that different generic instantiations of the same class share a single runtime class object, which only has space for a single ClassVar of one type. You'd get type checker behavior like this:
class Foo[T]: x: ClassVar[T] assert_type(Foo[int].x, int) assert_type(Foo[str].x, str)
But Foo[int].x and Foo[str].x refer to the same runtime object, which can't be both an int and a str.
The practical use cases people come up with tend to involve generic base classes with non-generic child classes. That does make sense at runtime and in the type system, so it would be useful to allow.
Beta Was this translation helpful? Give feedback.
All reactions
-
If we wanted to give up slightly less safety we could also think about only allowing this pattern in abstract classes, this is still not completely safe, since accessing the attribute on the abstract base class would still be almost certainly incorrect and it is legal to access attributes on an abstract class, but it is more esoteric than the likely way this will actually be used (i.e. as a faux read-only property shared by all instances)
Since this is still a fairly strict error regardless, it could be one of the optional checks enabled through --strict.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 4
-
Sorry for necroposting however I have encountered a scenario where I feel a generic ClassVar is justified, hopefully this is not an XY problem.
I have a few DAO models that have unique ids and I wanted to create a class which serves as a lazy getter, as I otherwise would see myself writing very similar properties on multiple classes. The way I implemented this is by creating a base class which implements the protocols, to be inherited from for each DAO class to be wrapped, effectively acting as an explicit specialization of the base class, with the wrapped class stored as a class variable, as it is needed to get the objects from their ids. ie something like:
class Reference[TDAO]: DAO: ClassVar[type[TDAO]] def __init_subclass__(cls, dao: TDAO): cls.DAO = dao # __get__, __set__ etc implementations # somewhere else class CookieDAO: ... class CookieReference(Reference[CookieDAO], dao=CookieDAO): pass class CookieHolder: cookie = CookieReference()
I have though of what seems like a hack to me, that is creating a separate factory class and to pass the wrapped type as an argument when creating the reference. This doesn't feel that pythonic to me and also feels wrong from a type perspective.
class Reference[TDAO]: DAO: type[TDAO] def __init__(self, dao: TDAO): self.dao = dao # __get__, __set__ implementations class ReferenceFactory[TDAO]: DAO: type[DAO] def __init__(self, dao: TDAO): self.dao = dao def __call__(self) -> Reference[DAO]: return Reference[DAO](self.dao) # somewhere else class CookieDAO: ... cookie_reference_factory = ReferenceFactory(CookieDAO) # or pretend its a type ie CookieReference class CookieHolder: cookie = cookie_reference_factory()
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 4
-
In this scenario, you could create type hints for dynamic classes that have fixed class variables but do not affect other dynamic classes.
from collections.abc import Callable from types import new_class from typing import ClassVar, Protocol, reveal_type from pydantic import BaseModel class DynamicClass[ModelT: BaseModel, T](Protocol): model: ClassVar[type[ModelT]] # type: ignore params: dict[str, object] # instance var @staticmethod # ClassVar[Callable] needs @self def handler(model: ModelT, /) -> T: ... def __init__(self, params: dict[str, object]) -> None: ... def callhandler(self) -> T: ... def dynamic_init[T]( self: DynamicClass[BaseModel, T], params: dict[str, object] ) -> None: self.params = params def dynamic_callhandler[T](self: DynamicClass[BaseModel, T]) -> T: return self.handler(self.model(**self.params)) class DynamicMeta(type): model: type[BaseModel] def dynamic_class[ModelT: BaseModel, T]( name: str, model: type[ModelT], ) -> Callable[[Callable[[ModelT], T]], type[DynamicClass[ModelT, T]]]: def inner(handler: Callable[[ModelT], T]) -> type[DynamicClass[ModelT, T]]: return new_class( name, kwds={"metaclass": DynamicMeta}, exec_body=lambda ns: ns.update( model=model, handler=staticmethod(handler), __init__=dynamic_init, callhandler=dynamic_callhandler, ), ) return inner class FooModel(BaseModel): value: object @dynamic_class("Foo", FooModel) def foo(model: FooModel) -> str: return str(model.value) inst = foo({"value": 5}) resp = inst.callhandler() reveal_type(inst) reveal_type(resp)
mypy
Revealed type is "DynamicClass[FooModel, str]" Revealed type is "str"
pyright
Type of "inst" is "DynamicClass[FooModel, str]" Type of "resp" is "str"
at runtime
Runtime type is 'Foo'
Runtime type is 'str'
Beta Was this translation helpful? Give feedback.
All reactions
-
It appears that mypy supports to some extent this now: https://github.com/python/mypy/pull/19292/files
https://mypy-play.net/?mypy=latest&python=3.12&gist=e8cd136915319b3ff828467e435f17e6
would be great if Pyright can also
Beta Was this translation helpful? Give feedback.