5

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 
3
  • 1
    PyCharm is correct to warn here. Since do only takes f: (A) -> None functions, it could merely call f(A()) without violating its contract to f. something_a works with that, something_b does not (it requires "at least" a B). You might want to read up on co- and contravariance. Commented Jan 27, 2021 at 21:27
  • I think that f(B()) is perfectly valid (not violating Callable[[A], None]). I do see your point however that if I allow do to accept something_b, than it could pass it an A, which could "break" something_b's contract. Thanks, Missed that. Commented Jan 27, 2021 at 21:40
  • Yes, the code itself as-is is valid, but do actually takes a f: (B) -> None. Commented Jan 28, 2021 at 7:18

1 Answer 1

4

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 
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.