Type hinting for a Callable that supports subclasses?

Viewed 266

I would like to type-hint a method that may receive a Callable that operates on A or sub-classes of A:

def do(f: Callable[[A], None])

I've also tried:

def do(f: Callable[[typing.Type[A]], None])

A full dummy example looks like this:

class A:
    n = 1

class B(A):
    n = 2

def something_a(x: A):
    print(x.n)

def something_b(x: B):
    print(x.n)

def do(f: Callable[[A], None]):
    f(B())

do(something_a)  # ok
do(something_b)  # PyCharm type check warnning
1 Answers

As MisterMiyagi pointed out - The type check warning is correct.

B inherits from A, so something_a(x: A) can accept both A and B, whereas something_b(x: B) can only accept B. (covariance)

If we pass something_b(x: B) to do(f: Callable[[A], None]), we allow something_b(x: B) to accept an A - which it can't. So something_b(x: B) can't substitute something_a(x: A). On the other hand passing something_a(x: A) to a function that expects a Callable[[B], None] would've been okay. (contravariance)

What to do? - use Generics

AType = TypeVar('AType', bound=A)

def do_fixed(f: Callable[[AType], None]):
    pass

do_fixed(something_a)  # ok
do_fixed(something_b)  # ok

While we do get a warning for passing other type of callable:

class C:  # not derived from A
    n = 1

def something_c(x: C):
    print(x.n)

do_fixed(something_c)  # type check warning

But we still need to make sure we're not passing A to something_b, which can be achieved by:

def do_safer(f: Callable[[AType], None], v: AType):
    f(v)
    
do_safer(something_a, A())  # ok
do_safer(something_a, B())  # ok
do_safer(something_b, A())  # type check warning
do_safer(something_b, B())  # ok
Related