can't compose @future_safe coroutines that specify picky exceptions
Given coroutines coro1, coro2:
@future_safe(exceptions=(ConnectionError,))
async def coro1(c: int | None) -> int:
if c is None:
raise ConnectionError("not connected")
await asyncio.sleep(0) # emulate I/O
return c
@future_safe(exceptions=(ZeroDivisionError,))
async def coro2(n: int) -> float:
await asyncio.sleep(0) # emulate I/O
return 1 / n
I want to combine them via bind: coro1(c).bind(coro2). It results in type error:
$ uvx --with 'returns[compatible-mypy]==0.25.0' mypy mre.py; ./mre.py
mre.py:30: error: Argument 1 to "bind" of "FutureResult" has incompatible type "Callable[[int], FutureResult[float, ZeroDivisionError]]"; expected "Callable[[int], KindN[FutureResult[Any, Any], float, ConnectionError, Any]]" [arg-type]
where mre.py:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "returns >=0.25.0",
# ]
# ///
from typing import assert_never
from returns.result import Success, Failure
from returns.io import IOSuccess, IOFailure
from returns.future import future_safe
@future_safe(exceptions=(ConnectionError,))
async def coro1(c: int | None) -> int:
if c is None:
raise ConnectionError("not connected")
await asyncio.sleep(0) # emulate I/O
return c
@future_safe(exceptions=(ZeroDivisionError,))
async def coro2(n: int) -> float:
await asyncio.sleep(0) # emulate I/O
return 1 / n
async def run() -> None:
for c in [2, 0, None]:
match await coro1(c).bind(coro2):
case IOSuccess(Success(r)):
assert r == 1 / 2, r
case IOFailure(Failure(ZeroDivisionError(args=(msg,)))):
assert msg == "division by zero", msg
case IOFailure(Failure(ConnectionError(args=(msg,)))):
assert msg == "not connected", msg
case _ as unreachable:
assert_never(unreachable) # type: ignore[arg-type]
if __name__ == "__main__":
import asyncio
asyncio.run(run())
If picky exceptions are removed:
@future_safe
async def coro1(c: int | None) -> int: ...
@future_safe
async def coro2(n: int) -> float: ...
I get the desired behavior that coroutines are composed without type errors but these declarations catch too much.
Will https://github.com/dry-python/returns/blob/master/returns/pointfree/unify.py work for you?
That's correct that you can't unify by default. You either can:
- Define common exception type like
type MyError = ConnectionError | ZeroDivisionErrorand use it likex: IOResult[float, MyError] = await coro1(c).bind(coro2) - Use
IOResultEif you don't care about precise exceptions types - Use
unify:)
match await unify(coro2)(coro1(c)): passes the type check. The issue is resolved. It would be perfect for my use-case if the type checking worked with lambdas. For now, it is a lot of unify(partial(Class.method, a=a, b=b, c=c))(container_with_self) that works but verbose.
I tried union type and it worked:
ErrorType = ConnectionError | ZeroDivisionError
Coro1Type = Callable[[int | None], FutureResult[int, ErrorType]]
Coro2Type = Callable[[int], FutureResult[float, ErrorType]]
but the coupling it introduces between coro1 and coro2 is not always desired.
Initially, I used FutureResultE that is too permissive.
The type check passes with the original match await coro1(c).bind(coro2) code on https://github.com/dry-python/returns/pull/1975
uvx --with 'git+https://github.com/dry-python/returns.git@refs/pull/1975/head' --with mypy mypy mre.py; ./mre.py
Success: no issues found in 1 source file
No errors on lambdas too.