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

Returing generic type from indirect factory methods? #2159

Unanswered
pmhahn asked this question in Q&A
Discussion options

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.

You must be logged in to vote

Replies: 1 comment 1 reply

Comment options

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.

You must be logged in to vote
1 reply
Comment options

If you paste an AI generated answer, at least have the courtesy to properly format the reply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

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