openlang

Why can't type variables appear inside ClassVar?

According to PEP-526 (https://peps.python.org/pep-0526/#class-and-instance-variable-annotations)

Note that no type variable may occur within a ClassVar argument, at any nesting depth: ClassVar[T] is invalid, as is ClassVar[List[Set[T]]], whenever T denotes a type variable.

What is the reason for that limitation? The PEP doesn't explain it; maybe the Typing github archives contain the answer…

I ran into this when annotating a generic base class with a pair of members, an instance attribute containing a typed instance and a class attribute containing that instance's type:

class _TransportBase[Conn: (httpx.Client, httpx.AsyncClient), Cred: httpx.Auth](ABC):
    base_url: str
    connection: Conn
    _credential: Cred
    _credential_type: ClassVar[Type]  # intended as ClassVar[Type[Cred]]

    @property
    def credential(self) -> Cred:
        return self._credential

    @credential.setter
    @abstractmethod
    def credential(self, incoming: Any):
        if not isinstance(incoming, self._credential_type):
            self._credential = self._credential_type(incoming)
        else:
            self._credential = incoming

With ClassVar limited this way, I must choose between explicitly declaring this attribute class-level and showing how its type relates to the type of _credential.

This early design is bound to change during revision. I'd welcome suggestions, though the question I'm asking here concerns ClassVar.

The conversation continues 2

Follow the reasoning
  1. Permalink

    A ClassVar belongs to the class itself, rather than separately to every choice of generic parameters.

    For another illustration, consider implementing a generic linked list.

    @dataclass
    class Link[T]:
        item: T
        following: Self | None = None
    
    class Chain[T]:
        start: ClassVar[Link[T] | None] = None
    
        def __str__(self) -> str:
            owner = type(self)
            items = list[T]()
            cursor = owner.start
            while cursor:
                assert isinstance(cursor, Link)  # assist the typechecker
                items.append(cursor.item)
                cursor = cursor.following
            return f'{owner.__qualname__}({', '.join(map(repr, items))})'
    
    numbers = Chain[int]()
    words = Chain[str]()
    
    Chain[int].start = Link[int](3, Link[int](4))
    print(numbers)  # 3, 4
    Chain[str].start = Link[str]('hello', Link[str]('world'))
    print(words)  # 'hello', 'world'
    print(numbers)  # unexpectedly! 'hello', 'world'

    The int list and the str list use the very same .start. Only a single one exists.

    To me that makes a TypeVar inside a ClassVar rather pointless.

  2. Permalink

    Right, I think I understand the reasoning behind that now.

    I intended ABC implementations to replace the ClassVar with a narrower type, with the ABC's declaration serving mainly as documentation because that attribute would never have an actual value on the ABC.

    Another source of confusion was that authlib uses httpx2, whereas my code used httpx, giving identically named but technically distinct types. Once I'd updated that dependency, I was able to use the base class directly instead of trying to parameterize over it in the ClassVar annotation.

Other sides of the same question

  1. How can I subclass a class inside another class?

    Hi, In a package whose code is outside my control, there is a hierarchy of classes arranged as follows: I'm trying to add a w…

    Replies 1Latest activity scala
  2. Comparing have with let bindings in Lean4

    I've observed that these two expressions and behave identically. This also happens in theorem proofs. Are they interchangeabl…

    Replies 3Latest activity lean-4
  3. TypeScript Map initialization with string array entries

    I'm creating a Map whose entries each hold an array of strings. How can I declare its type and initialize it together in one…

    Replies 2Latest activity typescript