Skip to content

Warn about unused Awaitables (and potentially other objects) #2499

Open

Description

@JelleZijlstra

This code:

async def f() -> None:
    pass
async def g() -> None:
    f()

is incorrect because f() isn't awaited. This will produce a runtime warning with the right debug settings, but mypy doesn't give any errors.

It would be nice if mypy could catch this, since I think it's a pretty common error in async code. This could be generalized to warning on other types or function return values that shouldn't be ignored (maybe open?), similar to __attribute__((warn_unused_result)) in GCC.

Editing in a 2025 update to the OP

Try --enable-error-code unused-awaitable --enable-error-code truthy-bool and see if that catches the thing you want it to catch

Activity

  1. gvanrossum commented on Nov 27, 2016

    @gvanrossum
    Member

    I'm pretty sure we've got another similar issue somewhere, but I can't find it. (About results that should never be discarded -- perhaps uncalled functions?)

    I'm not sure how easy this would be to do -- especially not the generalization, which probably would require us to invent some acceptable syntax for marking functions whose result should never be ignored.

  2. Daenyth commented on Nov 20, 2017

    @Daenyth

    I think skipping generalization and focusing on Awaitable first would still catch bugs. Once there's a second case people want to specialize it'll be easier to see what needs to be made general

  3. njsmith commented on Sep 17, 2018

    @njsmith

    Awaitable is a bit special too, because "is the value used" isn't quite the right check. For example, here's one of the cpython bots that had a missing await:

    python/cpython#9168 (comment)

    The code was something like f"Sorry, {get_participants()}, ...", but should have been f"Sorry, {await get_participants()}, ...". So the awaitable value was used, and was even used in a type-valid way (pretty much any python object can be interpolated into an f-string), but clearly not what was intended. On the other hand, in asyncio, passing an awaitable into a function like asyncio.gather is a totally normal way to consume it.

    I was looking at this recently in the context of Trio, where the rules for using awaitables are much simpler: basically just, any call returning an awaitable should have an await present, full stop. This is obviously too simple to become a general always-on feature in mypy, since it won't work for asyncio users, but it'd be nice to have it as an optional feature or a plugin or something. I made some more detailed notes here on what exactly we'd want and how it might work: python-trio/trio#671

    Is it possible to have a mypy plugin that adds a check like this? And can anyone point me at roughly where in the type checking process we'd want to be to perform a check like that? (I assume somewhere in mypy/checker.py?)

  4. ilevkivskyi commented on Sep 17, 2018

    @ilevkivskyi
    Member

    @njsmith

    Is it possible to have a mypy plugin that adds a check like this?

    I am not sure writing it as a plugin is the best solution. This will need addition of at least one new plugin hook. I think a better idea is to just add a flag to mypy. This looks like a common scenario. Also the implementation in mypy is very simple. We can add a flag is_awaited on CallExpr that will be populated during semantic analysis (by enclosing visit_await). An then an error can be given in visit_call_expr in checkexpr.py if the inferred resulting type is Awaitable, but the is_awaited flag is not set.

  5. njsmith commented on Sep 17, 2018

    @njsmith

    That would probably be the easiest approach, yeah. But... this check really is only useful to projects using trio, not to projects using asyncio. Is mypy interested in carrying features like that?

  6. ilevkivskyi commented on Sep 18, 2018

    @ilevkivskyi
    Member

    Is mypy interested in carrying features like that?

    I have heard similar things from other people. We can start with something, and then maybe generalize the flag to be tunable, for example:

    • --enforce-await=none (default) does nothing
    • --enforce-await=global (hard to implement) will complain only if a coroutine is never awaited
    • --enforce-await=local (easy to implement) will complain if a coroutine is not immediately awaited.

    But we can start just with a binary flag --enforce-await and then iterate on the result later.

  7. JukkaL commented on Sep 18, 2018

    @JukkaL
    Collaborator

    I don't like the idea of just adding another opt-in warning for this. I'd much prefer if we could have something that is enabled by default, even if it doesn't catch all errors. There's a risk that the majority of users would not realize that such an option exists.

    What about always rejecting code such as the original example, as the first iteration of this feature:

    async def f() -> None:
        pass
    async def g() -> None:
        f()  # Should be an error

    Is there ever a good reason to write f() without doing anything with the result? This wouldn't reject things like x = f(), only bare f(). x = f() would likely generate an error already if the code does anything with x later on.

    We could later warn about things like "{}".format(should_be_awaited()) -- again, this is something that would hardly ever be right. We could special case some common operations that work with arbitrary objects, such as str.format, % formatting, str(x) and f-strings. We could extend this to check for things like "%s" % foo.method as well (i.e., forgot to call a method).

    Later, we could consider adding a trio-specific stricter check (possible as a plugin).

  8. njsmith commented on Sep 19, 2018

    @njsmith

    I'd much prefer if we could have something that is enabled by default, even if it doesn't catch all errors. There's a risk that the majority of users would not realize that such an option exists.

    In Trio's case, we'd probably make it enabled by default in our official project template and advertise it prominently in the docs (we already have a whole discussion of this issue right in the tutorial). But yeah, I know what you mean.

    Is there ever a good reason to write f() without doing anything with the result?

    If f returns a coroutine object, then you're correct, a bare f() is always a mistake. If f() returns a Future, then a bare f() is a relatively normal thing to write. So I guess a check like this keyed off Coroutine rather than Awaitable would make sense.

    We could later [...] later [...]

    I hear where you're coming from. But, it is frustrating that mypy has all the information to do a very straightforward, precise, and high-coverage check here, but it's blocked because we can't get at it :-/. Would there be any simple, acceptable change to the plugin interface that would make it possible to implement this as a plugin relatively soon?

    It looks to me like the two main blockers are:

    • You can define hooks that run on function and method calls, but any given function/method can only have 1 such hook. Here we want a hook that runs on all functions+methods, so we can do that, but if we do then it will effectively disable any other plugins that want to use those hooks. Maybe there could be some way to run multiple hooks in sequence? For the function and method hooks in particular it doesn't look like this would be terribly complicated semantically. This does create some issue about what order to run hooks in. (Ideally the Awaitable check would run last, I guess, so it can see the inferred ret_type after all other hooks have run?) But in practice it may not matter much. For this plugin, the only time it would matter would be if another plugin changed a ret_type from Awaitable to non-Awaitable, or vice-versa, and that seems rare.

    • There's no way for the hook to tell whether a given call is the immediate child of an await node. I guess this could be hacked into the Context?

  9. JukkaL commented on Sep 19, 2018

    @JukkaL
    Collaborator

    Would there be any simple, acceptable change to the plugin interface that would make it possible to implement this as a plugin relatively soon?

    Small plugin API changes are typically not controversial, so this is a possibility.

    Maybe there could be some way to run multiple hooks in sequence?

    That would be one way to do it, but ordering issues would make this a bit awkward, as you point out. Another option would to add a new "check only" hook that is called after the signature hooks have already run and we have a final signature. The hook wouldn't be able to change the signature but it can perform additional checks, and it's guaranteed to run after the signature-related hooks. We could allow multiple plugins to define these hooks for a function.

    There's no way for the hook to tell whether a given call is the immediate child of an await node. I guess this could be hacked into the Context?

    We'd need another attribute in the relevant context named tuple, I guess.

    Can you create a new issue for the plugin feature? We can then focus on the non-trio/non-plugin aspects in this issue.

  10. njsmith commented on Sep 20, 2018

    @njsmith

    Can you create a new issue for the plugin feature? We can then focus on the non-trio/non-plugin aspects in this issue.

    OK, forked this off into #5650 and replied there.

  11. Dreamsorcerer commented on Jun 8, 2021

    @Dreamsorcerer
    Contributor
    * `--enforce-await=local` (easy to implement) will complain if a coroutine is not _immediately_ awaited.
    

    What about a local check, with the added assumption that a function expecting an Awaitable is likely to await on it? Plus a tunable unknown state when not enough typing information is available (parameter is Any).

    This would automatically make functions like asyncio.gather() work without needing to special case them. I'm imagining that cases where a function expects an Awaitable and then doesn't await on it are going to be very rare (plus if they are within the user's code it would still highlight an error within that function).

    For example:

    async def do_something(): ...
    
    async def func_with_await(foo: Awaitable):
        return foo  # Error: foo not awaited
    
    async def unknown_func(foo: Any):
        print(foo)
    
    async def main():
        # These start as awaited=False
        a = do_something()
        b = do_something()
        c = do_something()
        d = do_something()
        e = do_something()
    
        await func_with_await(a)  # a.awaited=True
        asyncio.create_task(b)  # b.awaited=True
        await unknown_func(c)  # c.awaited=Unknown
        print(d)  # d.awaited=False
        # Error: e.awaited=False
    

    Whenever the awaited state is False it should error, and if True it should not error. When it is in the unknown state, this should error depending on a tunable flag (similar to the various Any warnings that are configurable).

  12. Dreamsorcerer commented on Nov 8, 2021

    @Dreamsorcerer
    Contributor

    Is there likely to be any movement on this? I've been hitting this problem a lot lately, trying to refactor a bunch of sync code to be async and struggling to find every location that needs an await.

  13. JelleZijlstra commented on Nov 8, 2021

    @JelleZijlstra
    MemberAuthor

    As you see, not much is happening.

    I did implement this check in Quora's typechecker, pyanalyze (https://github.com/quora/pyanalyze); you can see the relevant tests at https://github.com/quora/pyanalyze/blob/26e622e249d5b101323647a29ede386d4de06f1e/pyanalyze/test_name_check_visitor.py#L1868. It is not nearly as well-tested as mypy, though, but feel free to give it a try.

  14. 6 remaining items

  15. Dreamsorcerer commented on Mar 29, 2022

    @Dreamsorcerer
    Contributor
    * provide a `cast`-like method (implemented as `identity`) to signal to mypy "no, really, I don't want to await this" and locally silence the warning
    

    I think you have basically just described the purpose of # type: ignore comments.

    * if an awaitable is not awaited but is passed to a method whose parameter is _typed_ to receive an awaitable, do not warn
    

    This is what I proposed above (#2499 (comment)), with an option to change whether an error occurs when passed to a function with Any.

  16. graingert commented on Mar 29, 2022

    @graingert
    Contributor

    anyio has the concept of DeprecatedAwaitables that warn when you await them - they are list, float and Noneish subclasses otherwise. Is the advice to do this?

    class DeprecatedAwaitable:
         if not TYPE_CHECKING:
            def __await__(self):
                warnings.warn(..., stacklevel=2)
                return None
                yield
  17. jhance commented on Mar 29, 2022

    @jhance
    Collaborator

    I think trying to find whether all possible control flows await an awaitable is difficult for a type checker. How does it know if I pass it to a function whether that function will await it or not?

    The statement that passing it to a function is likely to await it is not true. For example appending to a list

  18. Dreamsorcerer commented on Mar 29, 2022

    @Dreamsorcerer
    Contributor

    I think trying to find whether all possible control flows await an awaitable is difficult for a type checker. How does it know if I pass it to a function whether that function will await it or not?

    The statement that passing it to a function is likely to await it is not true. For example appending to a list

    How frequently do you think a function will explicitly declare the type as Awaitable and then not await on it (either directly or through another function)? My assumption is that this will be very rare in real world use cases, likewise a typesafe function that doesn't accept an Awaitable (e.g. object) is very unlikely to await on it. If this works in real world use cases 99% of the time, then it's definitely worth putting in.

    If a function accepts Any then we have no idea if it will await or not, so having a strictness setting to warn or not in these cases is helpful. We can default to not warning to avoid false positives, but people looking for something stricter/safer can enable warnings in this case (see the different examples in my comment).

  19. Dreamsorcerer commented on May 12, 2022

    @Dreamsorcerer
    Contributor

    That just handles unused awaitable expressions. Something as simple as a = foo() won't trigger the warning.

    I think we still need to have something like #2499 (comment) to cover a lot more cases. It's a bit more complex as you need to track the awaited state through a function in order to see if it was awaited by the end, but would catch a lot more mistakes.

  20. jstasiak commented on May 12, 2022

    @jstasiak
    Contributor

    a = foo() where a remains unused should be covered by flake8 or other linters already under the unused variable umbrella (granted, maybe it'd be nice to have it in Mypy as well)

  21. Dreamsorcerer commented on May 12, 2022

    @Dreamsorcerer
    Contributor

    a = foo() where a remains unused should be covered by flake8 or other linters already under the unused variable umbrella (granted, maybe it'd be nice to have it in Mypy as well)

    That only works if the variable is unused. You can then do print(a) or similar and flake8 will no longer catch it as unused and mypy will not catch it as unawaited.

  22. tback commented on Mar 19, 2024

    @tback

    Can mypy be configured to require type hints for unawaited assigns?

    async def do_something(): ...
    
    async def main():
        # These start as awaited=False
        a = do_something()  # ERROR
        b : Awaitable = do_something()  # OK
        c = await do_something()  # OK
  23. Dreamsorcerer commented on Mar 19, 2024

    @Dreamsorcerer
    Contributor

    That is not an unusual thing to do though, so not sure how many people would find such an option useful.

  24. tback commented on Mar 19, 2024

    @tback

    My reasoning behind this request is to make delayed calls an active decision and to prevent errors from forgotten awaits.
    I'd assume that this would provide a robust solution for a couple of "simple" use cases while also being kind of easy to implement.

  25. blast-hardcheese commented on Dec 10, 2024

    @blast-hardcheese

    I ran into an issue recently with the following construct:

    async def a() -> bool:
      return False
    
    if a():
      pass

    Because a was truthy, it would always pass. The way I interpret the conversation thus far is to move towards disambiguation, guiding the user to codify their intent so typecheckers can pick up the slack MustUse[...] is an interesting idea (and fits my aesthetic) but I agree with the author that that's actually the default expectation in most cases.


    I think a sensible nudge here would be Awaitable[...] values requiring an explicit round-trip through a primitive type before being used in a primitive way. In my thinking currently, either bool or str. I don't think others apply.

    bool

    In my example above, if I truly wanted truthy behavior, I would change it to if bool(a()):.

    Interestingly, the None case is already handled:

    def a() -> None | Awaitable[...]:
      ...
    
    if await a():
      pass

    as we already emit

    error: Incompatible types in "await" (actual type "Awaitable[bool] | None", expected type "Awaitable[Any]")  [misc]
    

    str

    This would also sensibly apply to string interpolation. The following could be disambiguated by suggesting the user explicitly choose str(a()) or repr(a()) depending on what they are trying to accomplish.

    f"The value is currently {a()}."

    This would help with code reviews as well, where more seasoned developers would see the out-of-place str(...) or bool(...) and start prodding.


    I think broadly, if you're using a typechecker in Python at all you're opting in to removing classes of bugs.

    What we are trying to do here is just a formalization of intent, and shouldn't remove expressivity from the language.

    I could see pushback here being adding too much noise to existing programs, but I'd assert that the vast majority of cases where this would flag on existing code are actually already bugs (or debugging statements which can be patched by rote)

  26. hauntsaninja commented on Oct 11, 2025

    @hauntsaninja
    Collaborator

    Since people at work were talking about this... Mypy already supports a version of this; it will complain about OP's case out of the box. For others, you might need --enable-error-code unused-awaitable. Also --enable-error-code truthy-bool can catch some similar issues like the one from @blast-hardcheese .

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions