ruff icon indicating copy to clipboard operation
ruff copied to clipboard

Detect unawaited coroutines

Open alekssamos opened this issue 1 year ago • 10 comments

Hello. I fixed my code via ruff. Applied fix and unsafe fixes. But ruff did not find one error. I forgot to write await when calling an asynchronous function. Accordingly, I received RuntimeWarning never awaited... e.g.

async def another():
    ...

async def main():
    result = another()

Is there any way to do this, and if not, will it be taken into account in the new versions of ruff?

alekssamos avatar Feb 05 '24 15:02 alekssamos

I don't believe we check this now, but it looks like a reasonable rule to me.

charliermarsh avatar Feb 05 '24 15:02 charliermarsh

WIP

mikeleppane avatar Feb 08 '24 14:02 mikeleppane

Note that mypy already implements this rule with its unused-awaitable error code: https://mypy.readthedocs.io/en/stable/error_code_list2.html#check-that-awaitable-return-value-is-used-unused-awaitable.

That doesn't stop ruff implementing it as well -- but I feel like it might be a better fit for a type checker, as a type checker has the power to analyse multiple files at a time and track which calls return awaitable objects. Ruff doesn't (yet) have the power to do that (though it's obviously something we'd like to have).

Still, it can be hard to configure mypy to enable just that warning without also enabling other warnings that might be unwelcome -- and maybe we can provide a valuable lint here even while only looking at one file at a time.

AlexWaygood avatar Feb 09 '24 13:02 AlexWaygood

@AlexWaygood - I guess the rule as implemented here is actually a bit different (unclear if better or worse) than what's described in the Mypy docs, since this raises whenever you have an async method that doesn't await anything (like Clippy's unnecessary async rule), whereas the Mypy rule seems to raise on the awaitable rather than the async function. Is that correct?

charliermarsh avatar Feb 09 '24 14:02 charliermarsh

@AlexWaygood - I guess the rule as implemented here is actually a bit different (unclear if better or worse) than what's described in the Mypy docs, since this raises whenever you have an async method that doesn't await anything (like Clippy's unnecessary async rule), whereas the Mypy rule seems to raise on the awaitable rather than the async function. Is that correct?

Ah interesting -- I think what you describe is possibly what's originally being asked for in this issue, but the implementation that's been put forward for consideration in https://github.com/astral-sh/ruff/pull/9911 is more similar to mypy's unused-awaitable check than what you're describing

AlexWaygood avatar Feb 09 '24 14:02 AlexWaygood

Oh sorry, I hadn't looked at the code yet. I was imagining we'd look for async function definitions that lack any await or async iterator (like async for, etc.).

charliermarsh avatar Feb 09 '24 14:02 charliermarsh

Yeah it sounds like #9911 addresses a separate (but also valid) use case

zanieb avatar Feb 09 '24 16:02 zanieb

~~Hi, I've blocked out time to do this on Monday.~~

[Edit: I'll work on #9951, not this.]

plredmond avatar Feb 09 '24 18:02 plredmond

Do you know when this will be implemented? This would be a great addition to ruff!

Gu1nness avatar Aug 19 '24 12:08 Gu1nness

This will take a while. Ruff cannot currently resolve functions across files, which is a requirement for this rule to work reliable.

MichaReiser avatar Aug 19 '24 12:08 MichaReiser

I want to just add a strong +1 here for detecting non-awaited coroutines. Check out the discussion here:

https://www.reddit.com/r/Python/comments/1gulzjt/rewriting_4000_lines_of_python_to_migrate_to/

One of the commenters says they have a huge code base but can not (never?) migrate to async because it's just too hard to make sure all the code works together (like no missed awaits). This feature would make Ruff a must have tool for such upgrades.

However, I realize this is super hard. For example, this should be fine even though the coroutines are not directly awaited.

t1 = async_thing_1()
t2 = async_thing_2()

await task.gather(t1, t2)

mikeckennedy avatar Nov 19 '24 17:11 mikeckennedy

Note that the gather() example is only a difficulty for asyncio; neither anyio nor trio permit calling async functions without immediately awaiting them, in large part to avoid this kind of confusion. Ruff could be very valuable for these use-cases, even if the rule had to be disabled for asyncio codebases, or at least disable-if-retval-used.

(idiomatically, you'd call my_gather(partial(fn1, *args), partial(fn2, **kwargs), ...) - a list of functions)

Zac-HD avatar Feb 05 '25 01:02 Zac-HD

Hey @Zac-HD nice to see you here. I'm somewhat familiar with Trio and AnyIO. But how do you actually run two or more things concurrently without starting them before awaiting them?

for x in xes:
   await async_thing(x)

still only runs one thing at a time. And await gather(f(x), g(x), ...) is fine but can be tricky if you have to build up the args for each one in a loop.

mikeckennedy avatar Feb 08 '25 16:02 mikeckennedy

@mikeckennedy I believe the canonical example would be using TaskGroup.start_soon (https://anyio.readthedocs.io/en/stable/tasks.html#creating-and-managing-tasks)

zanieb avatar Feb 08 '25 18:02 zanieb

Would this be a feature better suited to https://github.com/astral-sh/ty ?

pirate avatar May 28 '25 17:05 pirate

Maybe in the future. Our focus right now is exclusively on typing features (things that are simply incorrect and not just suspicious)

MichaReiser avatar May 28 '25 20:05 MichaReiser

Big fan of this rule proposal!

JoshPaulie avatar Jul 12 '25 15:07 JoshPaulie