Skip to content

Fix crash on @types.coroutine with unannotated generator function#21436

Closed
Hrk84ya wants to merge 1 commit intopython:masterfrom
Hrk84ya:master
Closed

Fix crash on @types.coroutine with unannotated generator function#21436
Hrk84ya wants to merge 1 commit intopython:masterfrom
Hrk84ya:master

Conversation

@Hrk84ya
Copy link
Copy Markdown

@Hrk84ya Hrk84ya commented May 7, 2026

  • Combine type guard with isinstance check to avoid assertion on potentially None type
  • Add deferred AwaitableGenerator wrapping for unannotated functions after type inference
  • Apply generator type transformations (yield, receive, return) after check_func_item infers the function type
  • Ensures coroutines decorated with @types.coroutine or @asyncio.coroutine are properly wrapped even when type annotations are missing

Fixes #21426.

Problem

mypy 2.0 crashes with AssertionError in visit_decorator_inner when
type-checking an unannotated generator function decorated with
@types.coroutine (or @asyncio.coroutine):

import types

@types.coroutine
def f():
    yield
    return 1

AssertionError: assert isinstance(defn.type, CallableType)

This is a regression introduced in #21119 (split type-checking into interface and implementation phases). The AwaitableGenerator return-type wrapping was moved to the top of visit_decorator_inner, where it now runs before check_func_item. For unannotated functions, defn.type is still None at that point — it only gets inferred during check_func_item.

Fix

Replace the hard assert with an isinstance guard, and add a second (identical, idempotent) wrapping block after check_func_item to handle the case where the type was inferred rather than declared. The existing is_named_instance(..., "typing.AwaitableGenerator") check ensures the wrapping is never applied twice.

…pping

- Combine type guard with isinstance check to avoid assertion on potentially None type
- Add deferred AwaitableGenerator wrapping for unannotated functions after type inference
- Apply generator type transformations (yield, receive, return) after check_func_item infers the function type
- Ensures coroutines decorated with @types.coroutine or @asyncio.coroutine are properly wrapped even when type annotations are missing
Copilot AI review requested due to automatic review settings May 7, 2026 14:15
@ilevkivskyi ilevkivskyi closed this May 7, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes a mypy 2.0 crash when type-checking unannotated generator functions decorated with @types.coroutine / @asyncio.coroutine by deferring AwaitableGenerator wrapping until after type inference when needed.

Changes:

  • Replaces an assert on defn.type with an isinstance(..., CallableType) guard to avoid crashing when the type is not yet inferred.
  • Adds a second, idempotent AwaitableGenerator wrapping pass after check_func_item so unannotated coroutines get wrapped post-inference.
  • Keeps generator yield/receive/return transformations aligned with the inferred function type.
Comments suppressed due to low confidence (1)

mypy/checker.py:5737

  • The AwaitableGenerator wrapping logic is now duplicated in two places. This increases the risk of future drift (e.g., one block updated but not the other) and makes the control flow harder to reason about. Consider extracting the wrapping into a small helper (e.g., maybe_wrap_awaitable_generator(defn)) and calling it before and after check_func_item, keeping the CallableType guard and is_named_instance(...AwaitableGenerator) idempotency check inside the helper.
        if defn.is_awaitable_coroutine and isinstance(defn.type, CallableType):
            # Update the return type to AwaitableGenerator (unless we already did).
            # Note, this doesn't exist in typing.py, only in typing.pyi.
            if not is_named_instance(defn.type.ret_type, "typing.AwaitableGenerator"):
                t = defn.type.ret_type
                c = defn.is_coroutine
                ty = self.get_generator_yield_type(t, c)
                tc = self.get_generator_receive_type(t, c)
                if c:
                    tr = self.get_coroutine_return_type(t)
                else:
                    tr = self.get_generator_return_type(t, c)
                ret_type = self.named_generic_type("typing.AwaitableGenerator", [ty, tc, tr, t])
                typ = defn.type.copy_modified(ret_type=ret_type)
                defn.type = typ

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mypy/checker.py
Comment on lines +5747 to +5761
# For unannotated functions, defn.type is only available after check_func_item
# infers it. Apply the AwaitableGenerator wrapping now if it wasn't done above.
if defn.is_awaitable_coroutine and isinstance(defn.type, CallableType):
if not is_named_instance(defn.type.ret_type, "typing.AwaitableGenerator"):
t = defn.type.ret_type
c = defn.is_coroutine
ty = self.get_generator_yield_type(t, c)
tc = self.get_generator_receive_type(t, c)
if c:
tr = self.get_coroutine_return_type(t)
else:
tr = self.get_generator_return_type(t, c)
ret_type = self.named_generic_type("typing.AwaitableGenerator", [ty, tc, tr, t])
typ = defn.type.copy_modified(ret_type=ret_type)
defn.type = typ
Comment thread mypy/checker.py
Comment on lines +5747 to +5761
# For unannotated functions, defn.type is only available after check_func_item
# infers it. Apply the AwaitableGenerator wrapping now if it wasn't done above.
if defn.is_awaitable_coroutine and isinstance(defn.type, CallableType):
if not is_named_instance(defn.type.ret_type, "typing.AwaitableGenerator"):
t = defn.type.ret_type
c = defn.is_coroutine
ty = self.get_generator_yield_type(t, c)
tc = self.get_generator_receive_type(t, c)
if c:
tr = self.get_coroutine_return_type(t)
else:
tr = self.get_generator_return_type(t, c)
ret_type = self.named_generic_type("typing.AwaitableGenerator", [ty, tc, tr, t])
typ = defn.type.copy_modified(ret_type=ret_type)
defn.type = typ
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2.0 regression] INTERNAL ERROR: AssertionError in visit_decorator_inner on @types.coroutine generator function

3 participants