Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Reasons for not allowing generics in ClassVar? #1424

treykeown started this conversation in General
Discussion options

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?

You must be logged in to vote

Replies: 6 comments 2 replies

Comment options

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.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
2 replies
Comment options

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.

Comment options

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()
Comment options

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'
You must be logged in to vote
0 replies
Comment options

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

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /