Python 3 type hint for a factory method on a base class returning a child class instance
Question:
Let’s say I have two classes Base
and Child
with a factory method in Base
. The factory method calls another classmethod which may be overriden by Base
‘s child classes.
class Base(object):
@classmethod
def create(cls, *args: Tuple) -> 'Base':
value = cls._prepare(*args)
return cls(value)
@classmethod
def _prepare(cls, *args: Tuple) -> Any:
return args[0] if args else None
def __init__(self, value: Any) -> None:
self.value = value
class Child(Base):
@classmethod
def _prepare(cls, *args: Tuple) -> Any:
return args[1] if len(args) > 1 else None
def method_not_present_on_base(self) -> None:
pass
Is there a way to annotate Base.create
so that a static type checker could infer that Base.create()
returned an instance of Base
and Child.create()
returned an instance of Child
, so that the following example would pass static analysis?
base = Base.create(1)
child = Child.create(2, 3)
child.method_not_present_on_base()
In the above example a static type checker would rightfully complain that the method_not_present_on_base
is, well, not present on the Base
class.
I thought about turning Base
into a generic class and having the child classes specify themselves as type arguments, i.e. bringing the CRTP to Python.
T = TypeVar('T')
class Base(Generic[T]):
@classmethod
def create(cls, *args: Tuple) -> T: ...
class Child(Base['Child']): ...
But this feels rather unpythonic with CRTP coming from C++ and all…
Answers:
It is indeed possible: the feature is called TypeVar with Generic Self (though this is slightly misleading because we’re using this for a class method in this case). I believe it behaves roughly equivalently to the “CRTP” technique you linked to (though I’m not a C++ expert so can’t say for certain).
In any case, you would declare your base and child classes like so:
from typing import TypeVar, Type, Tuple
T = TypeVar('T', bound='Base')
class Base:
@classmethod
def create(cls: Type[T], *args: Tuple[Any]) -> T: ...
class Child(Base):
@classmethod
def create(cls, *args: Tuple[Any]) -> 'Child': ...
Note that:
- We don’t need to make the class itself generic since we only need a generic function
- Setting the TypeVar’s bound to ‘Base’ is strictly speaking optional, but is probably a good idea: this way, the callers of your base class/subclasses will at least be able to call methods defined in the base class even if you don’t know exactly which subclass you’re dealing with.
- We can omit the annotation on
cls
for the child definition.
Python 3.11 now has a Self type, in case anyone else also stumbled on this old question. mypy
support was added in version 1.0
, available on PyPI since Feb. 2023.
https://docs.python.org/3/library/typing.html#typing.Self
It’s a DRY way of annotating the return value of class method factories:
from typing import Self
from collections import defaultdict
class NestedDefaultDict(defaultdict):
def __init__(self, *args, **kwargs):
super().__init__(NestedDefaultDict, *args, **kwargs)
@classmethod
def from_nested_dict(cls, dict_) -> Self:
inst = NestedDefaultDict()
for key, val in dict_.items():
inst[key] = cls.from_nested_dict(val) if isinstance(val, dict) else val
return inst
Self’s great for method chaining APIs too. I’ve copied the Self-less example below from James Murphy’s video, and just added the 3 annotations.
# https://github.com/mCodingLLC/VideosSampleCode/blob/master/videos/095_method_chaining_and_self/method_chaining_and_self.py
class Player:
def __init__(self, name, position, fatigue=0):
self.name = name
self.position = position
self.fatigue = fatigue
def draw(self) -> Self:
print(f"drawing {self.name} to screen at {self.position}")
return self
def move(self, delta) -> Self:
self.position += delta
self.fatigue += 1
return self
def rest(self) -> Self:
self.fatigue = 0
return self
Let’s say I have two classes Base
and Child
with a factory method in Base
. The factory method calls another classmethod which may be overriden by Base
‘s child classes.
class Base(object):
@classmethod
def create(cls, *args: Tuple) -> 'Base':
value = cls._prepare(*args)
return cls(value)
@classmethod
def _prepare(cls, *args: Tuple) -> Any:
return args[0] if args else None
def __init__(self, value: Any) -> None:
self.value = value
class Child(Base):
@classmethod
def _prepare(cls, *args: Tuple) -> Any:
return args[1] if len(args) > 1 else None
def method_not_present_on_base(self) -> None:
pass
Is there a way to annotate Base.create
so that a static type checker could infer that Base.create()
returned an instance of Base
and Child.create()
returned an instance of Child
, so that the following example would pass static analysis?
base = Base.create(1)
child = Child.create(2, 3)
child.method_not_present_on_base()
In the above example a static type checker would rightfully complain that the method_not_present_on_base
is, well, not present on the Base
class.
I thought about turning Base
into a generic class and having the child classes specify themselves as type arguments, i.e. bringing the CRTP to Python.
T = TypeVar('T')
class Base(Generic[T]):
@classmethod
def create(cls, *args: Tuple) -> T: ...
class Child(Base['Child']): ...
But this feels rather unpythonic with CRTP coming from C++ and all…
It is indeed possible: the feature is called TypeVar with Generic Self (though this is slightly misleading because we’re using this for a class method in this case). I believe it behaves roughly equivalently to the “CRTP” technique you linked to (though I’m not a C++ expert so can’t say for certain).
In any case, you would declare your base and child classes like so:
from typing import TypeVar, Type, Tuple
T = TypeVar('T', bound='Base')
class Base:
@classmethod
def create(cls: Type[T], *args: Tuple[Any]) -> T: ...
class Child(Base):
@classmethod
def create(cls, *args: Tuple[Any]) -> 'Child': ...
Note that:
- We don’t need to make the class itself generic since we only need a generic function
- Setting the TypeVar’s bound to ‘Base’ is strictly speaking optional, but is probably a good idea: this way, the callers of your base class/subclasses will at least be able to call methods defined in the base class even if you don’t know exactly which subclass you’re dealing with.
- We can omit the annotation on
cls
for the child definition.
Python 3.11 now has a Self type, in case anyone else also stumbled on this old question. mypy
support was added in version 1.0
, available on PyPI since Feb. 2023.
https://docs.python.org/3/library/typing.html#typing.Self
It’s a DRY way of annotating the return value of class method factories:
from typing import Self
from collections import defaultdict
class NestedDefaultDict(defaultdict):
def __init__(self, *args, **kwargs):
super().__init__(NestedDefaultDict, *args, **kwargs)
@classmethod
def from_nested_dict(cls, dict_) -> Self:
inst = NestedDefaultDict()
for key, val in dict_.items():
inst[key] = cls.from_nested_dict(val) if isinstance(val, dict) else val
return inst
Self’s great for method chaining APIs too. I’ve copied the Self-less example below from James Murphy’s video, and just added the 3 annotations.
# https://github.com/mCodingLLC/VideosSampleCode/blob/master/videos/095_method_chaining_and_self/method_chaining_and_self.py
class Player:
def __init__(self, name, position, fatigue=0):
self.name = name
self.position = position
self.fatigue = fatigue
def draw(self) -> Self:
print(f"drawing {self.name} to screen at {self.position}")
return self
def move(self, delta) -> Self:
self.position += delta
self.fatigue += 1
return self
def rest(self) -> Self:
self.fatigue = 0
return self