Search code examples
pythontype-hinting

Type hints for generic class union Callable


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)

Solution

  • 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.