Open
Description
I've confirmed the following works in pyright and pyrefly - but not in mypy:
from dataclasses import dataclass
from typing_extensions import (
Awaitable,
Callable,
Generic,
TypeVar,
assert_type,
)
T = TypeVar("T")
@dataclass
class Agent(Generic[T]):
output_type: Callable[..., T] | Callable[..., Awaitable[T]]
async def coro() -> bool:
return True
def func() -> int:
return 1
# works
assert_type(Agent(func), Agent[int])
# mypy - error: Argument 1 to "Agent" has incompatible type "Callable[[], Coroutine[Any, Any, bool]]"; expected "Callable[..., Never] | Callable[..., Awaitable[Never]]" [arg-type]
coro_agent = Agent(coro)
# pyright, pyrefly - works
# mypy - error: Expression is of type "Agent[Any]", not "Agent[bool]"
assert_type(coro_agent, Agent[bool])
# works
assert_type(Agent[bool](coro), Agent[bool])
I want T
to be inferred as the ultimate return type of the awaitable if an async function is passed rather than a regular one, but I suppose it's ambiguous which side of the union is the best match.
It would be great to see this work in mypy, but I'm also open to suggestions to do this in a less ambiguous way!
- This is related to a new PydanticAI feature, if you're curious check out Support functions as output_type, as well as lists of functions and other types pydantic/pydantic-ai#1785 (comment)
Metadata
Metadata
Assignees
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
A5rocks commentedon May 23, 2025
As you note this is ambiguous. As a workaround overloads work.
I'm not sure if there's any principled way around this. I have been thinking about a "strict coloring" mode which treats async functions as different types than non-async ones, but that would be stricter than you would like and also not everyone will enable it.
Maybe mypy should special case specifically
T | Awaitable[T]
since that's the only case where I've seen this.DouweM commentedon May 23, 2025
@A5rocks I appreciate the quick response.
Naively, I'd imagine a general rule like "in case of multiple possible matches, pick the most specific one", which may be what pyright is doing (note that I haven't looked at the implementation).
Special casing
T | Awaitable[T]
would work for me, but I'm curious why we couldn't do that for anyT | Foo[T]
when givenFoo[T]
. I'd expect this to work, for example:mypy currently says this:
Note that pyright doesn't like this either, so maybe it is special casing
T | Awaitable[T]
. It complains aboutx
on linereturn x
, but it does let theassert_type
pass:A5rocks commentedon May 23, 2025
I imagine this wouldn't do well if there's multiple possible matches with same specificity, or even something like:
(You could imagine
A
asAwaitable
anda: T
asdef __await__(self) -> T
(iirc?) if you like)However it is an improvement in some cases so if we can isolate those that sounds fine. But also if we're adding special cases I would rather being specific eg only special casing
T | Awaitable[T]
.Maybe a better method is tracking the number of levels above the typevar and choosing the highest one? Or maybe discarding conflicting constraints in order of the union? Neither sound very performant of course.
DouweM commentedon May 23, 2025
@A5rocks Good point, a special case sounds reasonable then. Thanks for considering this!
A5rocks commentedon May 25, 2025
And BTW I saw you misinterpreted me in the comment for the PR:
__init__
can be overloaded.DouweM commentedon May 26, 2025
@A5rocks As don't think that'd work with the real
OutputType
, with theCallable[..., T | Awaitable[T]]
nested a few levels down:That means
output_type
can be a list of types and regular functions and async functions:Note that the use of
Sequence[...]
withtype[T]
also has us run into #19142.To use overloads, I think I'd need to define some new marker class like
OutputFunc
with overloads for regular functions and async functions. That's definitely an option, but would make the API a bit less clean, so if mypy is planning to fix this issue and theSequence[type[T]]
one, I'd rather keep it like this (which already works with pyright).Is there another option I'm missing?