twisted icon indicating copy to clipboard operation
twisted copied to clipboard

Better Deferred type hints for callbacks that return `_T|Deferred[_T]`

Open wRAR opened this issue 1 year ago • 4 comments

It looks like there are some cases where a callback that returns a type or a Deferred of the same type is not accepted by mypy.

The most basic case, which works fine:

from twisted.internet.defer import Deferred

def cb(result: int) -> Deferred[int] | int:
    ...

d: Deferred[int] = Deferred()
d.addCallback(cb)

This one doesn't:

from twisted.internet.defer import Deferred
from twisted.python.failure import Failure

def cb(result: int | Failure) -> Deferred[int] | int | Failure:
    ...

d: Deferred[int] = Deferred()
d2: Deferred[int] = d.addBoth(cb)

"Argument 1 to "addBoth" of "Deferred" has incompatible type "Callable[[int | Failure], Deferred[int] | int | Failure]"; expected "Callable[[int | Failure], Failure]" [arg-type]"

In this second snippet removing any detail of it (removing the d2 assignment or its type hint, removing either int or Deferred[int] from the callback return type, using addCallback instead of addBoth + removing Failure from the arg and the return value) removes the error, but simply replacing addBoth with addCallback doesn't. Removing the annotation from d2 and running reveal_type on it gives twisted.internet.defer.Deferred[builtins.object].

Another unrelated example:

from twisted.internet.defer import Deferred, maybeDeferred

def fn() -> int | Deferred[int]:
    ...

d: Deferred[int] = maybeDeferred(fn)

"Argument 1 to "maybeDeferred" has incompatible type "Callable[[], int | Deferred[int]]"; expected "Callable[[], Deferred[int]]" [arg-type]"

Here removing the annotation from d and running reveal_type on it gives twisted.internet.defer.Deferred[Union[builtins.int, twisted.internet.defer.Deferred[builtins.int]]].

wRAR avatar Jun 12 '24 18:06 wRAR

This is definitely a thing that happens, but I am not seeing how it is actionable — is there something twisted could change here?

glyph avatar Jun 12 '24 19:06 glyph

To be clearer: why is this a bug on Twisted rather than on mypy?

glyph avatar Jun 12 '24 19:06 glyph

I just assumed it's something that is missing in the Twisted type hints. As overloads for both addCallback/addBoth and maybeDeferred are quite complex it's not immediately obvious whether they support this or not. Do you think if it's possible to make sure Twisted is correct in this regard, by looking closer at its type hints?

wRAR avatar Jun 12 '24 19:06 wRAR

I just assumed it's something that is missing in the Twisted type hints. As overloads for both addCallback/addBoth and maybeDeferred are quite complex it's not immediately obvious whether they support this or not. Do you think if it's possible to make sure Twisted is correct in this regard, by looking closer at its type hints?

Having looked quite closely at those type hints when I last rewrote them, I believe that this is about as well as we can do. There are some limitations in Mypy which make it impossible to express some of the constraints. I would love for someone to point out a way to be more precise though, so I will leave this open if you're inclined to give it a shot yourself.

glyph avatar Jun 12 '24 23:06 glyph

An improvement here would be to separate what is logically the Functor part of addCallback (callbacks returning T - aka fmap) from what is logically the Monad part of addCallback (returning Deferred[T] - aka bind).

I expect any such transition would be quite long but could have a number of benefits in terms of code complexity (eliminating it) and performance (improving it).

exarkun avatar Jan 05 '25 16:01 exarkun