Given:
from typing import Callable, Generic, TypeVar
T = TypeVar("T")
class Factory(Generic[T]):
def __call__(self) -> T:
...
class TruthyFactory(Factory[bool]):
def __call__(self) -> bool:
return True
def falsey_factory() -> bool:
return False
def class_consumer(factory: type[Factory[T]]) -> T:
...
def function_consumer(factory: Callable[[], T]) -> T:
...
# type hint for cls_ret is `bool`
cls_ret = class_consumer(TruthyFactory)
# type hint for fn_ret is `bool`
fn_ret = function_consumer(falsey_factory)
What would the signature (type hints) look like for a function taking a unison of the two parameter types?
That is, a function signature of: def either_consumer(factory: ???) -> T: ...
I tried using type[Factory[T]] | Callable[[], T]
but it not work with the type of a subclassed Factory[T]
. The return type hint becomes the type of the class. I believe this is because a class matches the Callable specification - does this have higher affinity, is there a way of modifying this behaviour?
def either_consumer(factory: type[Factory[T]] | Callable[[], T]) -> T:
# needs more stringent boolean logic
if isinstance(factory, type):
return factory()()
return factory()
# type hint for cls_ret is wrongly `TruthyFactory`
cls_ret = either_consumer(TruthyFactory)
# type hint for fn_ret is correctly `bool`
fn_ret = either_consumer(falsey_factory)
Okay I found an answer (at least using pyright in vscode).
Setting the parameter type to Callable[[], T] | type[Factory[T]]
works, however type[Factory[T]] | Callable[[], T]
does not.
Additionally type[Callable[[], T] | Factory[T]]
works.
I prefer the second option, but I do not know if this is entirely correct or it is undefined behaviour that just happens to work. I can not canonically say why... Perhaps the "type" of a function reference is its signature according to the type system? The order of these parameters matters, I also assume type system matches the last matching type in the union?
The first option only works with pyright
.
The second option works woth pyright
and jetbrains'
. I have not tested mypy
.
Complete example:
from typing import Callable, Generic, TypeVar
T = TypeVar("T")
class Factory(Generic[T]):
def __call__(self) -> T:
...
class TruthyFactory(Factory[bool]):
def __call__(self) -> bool:
return True
def falsey_factory() -> bool:
return False
def either_consumer(factory: type[Callable[[], T] | Factory[T]]) -> T:
# needs more stringent boolean logic
if isinstance(factory, type):
return factory()()
return factory()
# type hint for cls_ret is correctly `bool`
cls_ret = either_consumer(TruthyFactory)
# type hint for fn_ret is correctly `bool`
fn_ret = either_consumer(falsey_factory)
Rearranging @chepner's answer works also but is unconstrained Callable[[], T | Callable[[], T]]
On more experimenting it seems this is inconsistent entirely.