-
Notifications
You must be signed in to change notification settings - Fork 288
-
I have a class-hierarchy and a matching class-hierarchy of managers for them:
from __future__ import annotations from typing import ClassVar, Generic, TypeVar, reveal_type class Dir: def walk(self) -> None: print("Dir") class Git(Dir): def walk(self) -> None: print("Git") def commit(self) -> None: print("Commit") T = TypeVar("T", bound=Dir) class Manager(Generic[T]): create: ClassVar[type[T]] # def create(self) -> T: raise NotImplementedError def create2(self) -> tuple[T, T]: return (self.create(), self.create()) class DirManager(Manager[Dir]): create = Dir # def create(self) -> Dir: return Dir() class GitManager(DirManager): # , Manager[Git] create = Git # def create(self) -> Git: return Git() reveal_type(DirManager().create()) # note: Revealed type is "mypy-generic-multiclass.Dir" reveal_type(DirManager().create2()) # note: Revealed type is "tuple[mypy-generic-multiclass.Dir, mypy-generic-multiclass.Dir]" reveal_type(GitManager().create()) # note: Revealed type is "mypy-generic-multiclass.Git" reveal_type(GitManager().create2()) # note: Revealed type is "tuple[mypy-generic-multiclass.Dir, mypy-generic-multiclass.Dir]"
Is it possible to get GitManager().create2() to also return (Git, Git) instead of (Dir, Dir) without explicitly overwriting create2() in every sub-class?
PS: pyright and pyrefly do not like the declaration of create: ClassVar[type[T]] and report
error: "ClassVar" type cannot include type variables (reportGeneralTypeIssues)
#1424 has some more information about that. Using the alternative def create(self) -> ... as hinted in the comments does not make a difference regarding the return types. mypy and ty accept it. See #1775 for hinting that.
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment 1 reply
-
You’re very close — this is a classic case of "the generic parameter is not flowing through the subclass hierarchy".
What’s going wrong
The key issue is here:
class DirManager(Manager[Dir]):
create = Dir
By inheriting from Manager[Dir], you are freezing T = Dir for all subclasses of DirManager.
So even though GitManager creates Git, the generic parameter is still Dir, and that’s why:
reveal_type(GitManager().create2())
tuple[Dir, Dir]
Type checkers are doing exactly what the type hierarchy tells them to do.
✅ Correct pattern: make the intermediate class generic too
You want the generic parameter to remain open until the leaf class.
from future import annotations
from typing import Generic, TypeVar
class Dir:
def walk(self) -> None: print("Dir")
class Git(Dir):
def walk(self) -> None: print("Git")
def commit(self) -> None: print("Commit")
T = TypeVar("T", bound=Dir)
class Manager(Generic[T]):
def create(self) -> T:
raise NotImplementedError
def create2(self) -> tuple[T, T]:
return self.create(), self.create()
🔑 Make DirManager generic instead of fixing T
class DirManager(Manager[T], Generic[T]):
pass
Concrete leaf classes finally bind the type
class GitManager(DirManager[Git]):
def create(self) -> Git:
return Git()
✅ Result (what you want)
from typing import reveal_type
reveal_type(GitManager().create())
reveal_type(GitManager().create2())
tuple[Git, Git]
This works correctly in mypy, pyright, and pyre.
Why this works
Manager[T] defines behavior
DirManager[T] preserves the generic parameter
GitManager finally binds T = Git
create2() stays fully generic and does not need to be overridden
This is the same pattern used by the standard library (collections, typing, asyncio, etc.).
About ClassVar[type[T]]
You’re also correct that:
create: ClassVar[type[T]]
is rejected by pyright/pyre — that’s intentional and specified behavior.
ClassVar cannot contain TypeVars, because it would imply per-instance specialization of a class attribute, which Python’s type system does not support.
Using a normal method override (as above) is the correct and portable solution.
Beta Was this translation helpful? Give feedback.
All reactions
-
👎 2
-
If you paste an AI generated answer, at least have the courtesy to properly format the reply.
Beta Was this translation helpful? Give feedback.