SyntaxWarning for await a unawaitable literal

Here is the idea for that warning if the code will await a literal:

async def a():
    await ()

will warning that tuple object can’t be awaited.

Reason

  1. The python has warning for 1(), 1[2]. The analysis for whether an operation can do on one type object for literal is easy.
  2. Unlike raise, assert, the await is an expression-level keyword so that it can appear anywhere. I just searched for that the python codes with await (. Only a half appeared at the start of the line. This will make it more difficult for maintainers to review.
  3. Unlike 1+"foo" (which python doesn’t and won’t warn), the objects after await often associate with system or internet operations. When the programmer write:
async def some_operation():
    await (
        # many codes
    )

but it changes to a tuple literal due to wrong parentheses or commas, finally the interpreter create the object in the tuple but can’t continue because the code try to await a tuple, something doesn’t work, which cause the serious consequences (data corruption, system anomaly). If the warning can do at the compiling, programmer can kill the process before the problem code running and avoid the problem above. Meanwhile, adding the warning for “await a” is also easier than “a+b”. The warning for “a+b” need two types and their opposite magic methods returned NotImplement which is almost impossible, while “await a” just check whether the type of a has __await__. It means that this change is low risk and high return.

Effect

It will just effect the python test (with -W error). To fix it, the code like:

async def a():
    await 1

change to:

async def a():
    x = 1
    await x

can fix the problem.

The normal project won’t be effect if the code is right at all.

1 Like

Historical reference. Existing syntax warnings about missed comma were added for very common error – when you have a sequence of items, one per line, then added more items, and forget to add a comma after the previous last item. For example:

a = [
    (1, 2, 3),
    (3, 4, 5)
    (6, 7, 8)
]

a = [
    [1, 2, 3],
    [3, 4, 5]
    [6, 7, 8]
]

This is a valid Python syntax, but an obvious error.

At that time, tracebacks were not so helpful, so it was was not easy to figure out what happened and where exactly was error. Warnings provided more accurate location information, and did it earlier. Now there are less reasons for adding such warnings, although they may still be useful in some cases.

BTW, this is why a comma is acceptable after the last item. But many code is written without it.

I never encountered an error of await applied to a tuple by accident. I do not know any plausible scenario for this. We don’t add syntax warnings for every syntactically valid but errorenous form, we only add them if they help to diagnose common user errors.

5 Likes

When it happened, the only way is asking programmer to check the comma.

In fact, just “did it earlier” is a great reason, especially for await because the operations after it are often dangerous if not finished:
With await, the moment the code continues execution after the line, external state may already have been modified.
Catching the mistake before running the coroutine (even if the pattern is rare) prevents the kind of failures that can no longer be undone afterward.

This kind of situation is best prevented by IDEs and type checking.
SyntaxWarning would not prevent other non-literal non-awaitables being awaited.

At runtime, awaiting on non-awaitables just raises a TypeError:

import asyncio
async def foo():
    await 42
asyncio.run(foo())    # TypeError: 'int' object can't be awaited

This failed await does not modify external state, but understandably await statements often occur within a sequence of async operations. Designing APIs that interact with external resources requies a lot more care in error handling than what a naive SyntaxWarning can provide.

I make a file named “await_warning.py” containing:

import asyncio
async def a(): ...
async def b(): ...
async def c():
    await (a(), b())

if __name__ == "__main__":
    asyncio.run(c())

I expected that pycharm or mypy will report the wrong code that await a tuple in function c. However, both of them not.

If the a or b change to some other type objects, they will be created and maybe modify external state. In this program it finally also warns for being never awaited:

Warning (from warnings module):
  File "C:\Users\hh180\OneDrive\Desktop\await_warning.py", line 5
    await (a(), b())
RuntimeWarning: coroutine 'b' was never awaited

Warning (from warnings module):
  File "C:\Users\hh180\OneDrive\Desktop\await_warning.py", line 5
    await (a(), b())
RuntimeWarning: coroutine 'a' was never awaited
Traceback (most recent call last):
  File "C:\Users\hh180\OneDrive\Desktop\await_warning.py", line 8, in <module>
    asyncio.run(c())
  File "C:\Users\hh180\AppData\Local\Programs\Python\Python314\Lib\asyncio\runners.py", line 204, in run
    return runner.run(main)
  File "C:\Users\hh180\AppData\Local\Programs\Python\Python314\Lib\asyncio\runners.py", line 127, in run
    return self._loop.run_until_complete(task)
  File "C:\Users\hh180\AppData\Local\Programs\Python\Python314\Lib\asyncio\base_events.py", line 719, in run_until_complete
    return future.result()
  File "C:\Users\hh180\OneDrive\Desktop\await_warning.py", line 5, in c
    await (a(), b())
TypeError: 'tuple' object can't be awaited

Note: in ast parse, what after the Await_kind is Tuple_kind which will be parsed as tuple type and easy to warn (Use the implement on my PC):

PS C:\Users\hh180\my_python\await_warning> ./python $a/await_warning.py
Running Release|x64 interpreter...
C:\Users\hh180\OneDrive\Desktop/await_warning.py:5: SyntaxWarning: 'tuple' object can't be awaited
  await (a(), b())
C:\Users\hh180\OneDrive\Desktop/await_warning.py:5: RuntimeWarning: coroutine 'b' was never awaited
  await (a(), b())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
C:\Users\hh180\OneDrive\Desktop/await_warning.py:5: RuntimeWarning: coroutine 'a' was never awaited
  await (a(), b())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Traceback (most recent call last):
  File "C:\Users\hh180\OneDrive\Desktop/await_warning.py", line 8, in <module>
    asyncio.run(c())
    ~~~~~~~~~~~^^^^^
  File "C:\Users\hh180\my_python\await_warning\Lib\asyncio\runners.py", line 204, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\hh180\my_python\await_warning\Lib\asyncio\runners.py", line 127, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\hh180\my_python\await_warning\Lib\asyncio\base_events.py", line 719, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "C:\Users\hh180\OneDrive\Desktop/await_warning.py", line 5, in c
    await (a(), b())
TypeError: 'tuple' object can't be awaited
PS C:\Users\hh180\my_python\await_warning> mypy $a/await_warning.py
Success: no issues found in 1 source file

There are better tools available.

basedpyright says:

“tuple[CoroutineType[Any, Any, None], CoroutineType[Any, Any, None]]” is not awaitable

I heard that PyCharm got support for basedpyright recently.

However, just one tool cannot mean that we need to force all project to use it to check. Some project cannot support pyright (for example: aioprocessing). If the project relies on the module that cannot support the basedpyright, it won’t use it to check whether a tuple follow an await.

If existing tools don’t do what you need, then you can make a tool that does what you need.

The point is not about any specific tool.

The point is:

This kind of situation is best prevented by IDEs and type checking.

This is because mypy does not check unannotated functions by default; you can modify this behavior with --check-untyped-defs. (You might consider this behavior questionable but this is not the place to discuss it.)

As for the overall issue, I agree with @storchaka that this doesn’t seem like a common issue and it’s not clear that a SyntaxWarning is worth it.

3 Likes

Yes. However, the PyCharm and VsCode aren’t, and mypy default doesn’t.

If you ask all of the programmers to “make a tool that does what they need”, regrettably, IT IS IMPOSSIBLE.

But it cause serious problem.

In fact, the package “aioprocessing” has proved that just “IDEs and type checking” cannot fix anything because sometimes the third party modules couldn’t fit the tools. The programmer will see too many issues reported by the tools (the module docs indicate that you need to write like this but tools think that it is wrong) then turn off the tools.

And the Python interpreter doesn’t make a syntax warning.

So why is changing the Python interpreter better than changing PyCharm or changing VSCode or changing mypy?

Changing the interpreter is better because:

  • :check_mark: It applies to every environment
  • :check_mark: Static tools cannot reliably detect awaitability
  • :check_mark: IDE support is inconsistent
  • :check_mark: Python already warns for similar “accidental literal” cases
  • :check_mark: The interpreter can detect this cheaply and accurately
  • :check_mark: It prevents dangerous side effects earlier

This is why moving the check into the interpreter provides a safer, more consistent, ecosystem-wide improvement that tools alone cannot achieve.

This is also why it’s so hard to convince people that it should be changed in the interpreter. Much much easier to get one third-party tool to change than to try to demonstrate that this needs to be changed for absolutely everyone.

How’s that going to be any better? The interpreter already gives a runtime error if you try to await a non-awaitable. If you want something better than that, you’re asking for static checking, whether from the compiler or a third-party tool. What’s the difference?

Good! That means that some can be better than others, instead of everything all being the same. We have different tools precisely because this allows them to be good at different things.

Yep, because the existing ones are very common. If you can demonstrate that the case you’re showing is also extremely common, or that the error messages produced are particularly hard to understand, then you have a case; but you haven’t shown either.

More so than static checking?

So that’s three times that you’ve said “I want static checking rather than run time”, but you haven’t explained why that means it has to be in the interpreter.

I’m confused. What’s impossible? Making better tools?

2 Likes

To see what the IDE’s attitude. I open an issue to ask VsCode to add this warning. I will pause here and see what’s the result of the issue.

Indeed they ought to, and that would be a good feature request for IDEs, type checkers. There might even be overlooked aspects in the typing spec that needs fixing.

But I don’t believe a SyntaxWarning at runtime is really necessary. Awaiting unawaitables already fails loudly at runtime with an exception.

If we were really going down that route of preventing type errors through syntax, we should tighten the formal syntax by restricting the kind of expressions accepted in await, raise and similar statements, and make every such occurrence of obviously unawaitable and unraisable stuff an official SyntaxError. After all, if something’s so obviously wrong (and not just being ambiguous) to cause a SyntaxWarning, then why not raise a full SyntaxError. But that’d amount to statifying part of a highly dynamic language, and I guess that’s rarely worth the extra complexity in parser and compiler implementation.

One question that we need to answer first is: What is an “awaitable” object exactly? I did some research on that a few years back - not sure if anything has changed since, but the answer to that questions might not be that clear.