šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python async/await Syntax

The concrete syntax and mechanics of async/await — coroutines, awaitables, and the common mistakes (forgetting await, calling async code from sync code) that trip up nearly everyone at first.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What does calling greet("Ada") return, without await, where greet is async def?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

The previous lesson covered the event loop conceptually; this one covers the syntax you actually write. async/await looks deceptively like ordinary synchronous code, which is exactly why its few sharp edges — a forgotten await, calling a coroutine without running it — are so easy to trip over.

1A Coroutine Function Behaves Like a Generator Function

async def greet(name: str) -> str: defines a coroutine function — and calling it, greet("Ada"), follows the exact same pattern established in the Generators lesson: the function body doesn't execute immediately. Instead, calling it returns a coroutine object, an inert representation of 'this computation, not yet started', which only actually runs once something explicitly drives it forward.

That 'something' is await. await greet("Ada"), used inside another async def function, is what actually executes greet's body, suspending the *calling* coroutine at that point (yielding control back to the event loop, exactly as covered in the previous lesson) until greet completes and produces its return value — which await then hands back as its own expression value, letting you write result = await greet("Ada") and use result exactly like an ordinary function's return value.

await can only appear inside another async def function (or directly at a REPL/notebook top level, a special case some environments support) — you cannot await something from ordinary synchronous code. The one sanctioned bridge from synchronous to asynchronous code is asyncio.run(main()), which creates an event loop, runs the given coroutine to completion, and is meant to be called exactly once, at your program's actual entry point.

āœ•
—
+
async def greet(name: str) -> str:
    return f"Hello, {name}!"

result = greet("Ada")
print(result)   # <coroutine object greet at 0x...> -- NOT 'Hello, Ada!'
print(type(result))  # <class 'coroutine'>
localhost:3000
Coroutine Lifecycle
greet("Ada") → coroutine object
await greet("Ada") → 'Hello, Ada!' — actually runs it

2The Silent Bug: A Forgotten await

Because calling a coroutine function without await is syntactically valid — it's just an expression that happens to produce a coroutine object — Python cannot treat a missing await as a hard error the way it would a genuine syntax mistake. greet("Ada") on its own line, inside main(), is legal Python: it creates a coroutine object and then, since nothing captures or awaits it, that object is immediately eligible for garbage collection, and greet's body never runs at all.

CPython does emit RuntimeWarning: coroutine 'greet' was never awaited when this happens (detected when the unused coroutine object is garbage collected), which is a genuinely useful diagnostic — but warnings are easy to miss in noisy log output, and by default don't halt execution or fail a test suite the way an exception would. This makes 'forgot an await' one of the most common asyncio bugs in practice: the code runs without crashing, some side effect just silently never happens.

The practical defense is largely tooling-based: ruff and other linters can statically flag a coroutine-returning call whose result is neither awaited nor otherwise used, catching the mistake before runtime; treating RuntimeWarnings as errors in test configuration (-W error::RuntimeWarning in pytest) turns the silent runtime warning into an immediate, loud test failure instead.

āœ•
—
+
async def main():
    result = await greet("Ada")  # actually runs greet(), waits for it, gets the return value
    print(result)  # 'Hello, Ada!'

asyncio.run(main())  # the ONE place you bridge sync code into async code
localhost:3000
Silent Failure Mode
greet("Ada") without await
RuntimeWarning, but no crash — the body simply never ran

3create_task: Starting Work Without Immediately Waiting

await coroutine runs a coroutine *and waits for it to finish* before your own code continues — sequential from the calling coroutine's point of view, even though other tasks can interleave in the meantime via the event loop. Sometimes you want to start a coroutine running *now*, continue doing other work, and only wait for its result later — exactly the shape asyncio.gather uses internally to run multiple coroutines concurrently.

asyncio.create_task(greet("Ada")) schedules the coroutine to start running on the event loop immediately (at the next opportunity the loop gets), and returns a Task object right away, without blocking. Your code can then do other work — including starting more tasks — and only actually wait for the result when it calls await task, at which point it either gets the already-completed result immediately, or suspends until the task finishes, exactly like awaiting the original coroutine directly would.

This distinction — await a coroutine directly (sequential) versus create_task() then await the task later (concurrent) — is the actual mechanism asyncio.gather builds on, and understanding it directly clarifies exactly why gather(fetch("A"), fetch("B")) runs both concurrently: internally, it wraps each coroutine in a task, letting all of them start running before waiting on any of their results.

āœ•
—
+
async def main():
    greet("Ada")  # BUG: missing await -- greet() never actually runs!
    # RuntimeWarning: coroutine 'greet' was never awaited

asyncio.run(main())
localhost:3000
Concurrent Scheduling
task = create_task(greet("Ada"))
Starts immediately — await task later retrieves the result

4Step-by-Step Breakdown

Forgetting a single 'await' is the single most common asyncio bug — and Python won't always tell you loudly when it happens. Let's build the muscle memory to catch it.

async def creates a coroutine FUNCTION. Calling it does NOT run the body -- it returns a coroutine OBJECT, similar to how calling a generator function doesn't run it either.

Checkpoint: What does calling greet("Ada") return, without await, where greet is async def?

  • →A coroutine object — the function body has not run yet
  • →The string 'Hello, Ada!' — the function runs immediately

await is what actually RUNS the coroutine and gets its result -- and it only works inside another async function (or at the top level in a REPL/notebook).

A forgotten await doesn't crash loudly -- it just silently returns a coroutine object instead of running it, which Python often warns about but easy to miss.

Checkpoint: What actually happens if you write greet("Ada") inside an async function without await?

  • →greet's body never runs at all — a coroutine object is created and immediately discarded
  • →greet runs synchronously as a normal side effect, just without returning its value

asyncio.create_task() schedules a coroutine to run CONCURRENTLY, without immediately awaiting it -- letting you start work and check on it later.

With individual coroutines under control, Task Groups give you structured, safer patterns for running many of them together.

Inspect a Real Coroutine Object. Finish check_coroutine_type(): calling an async function returns a coroutine object without running it.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Enable linter/test-suite checks that catch a coroutine call missing await

Since a missing await fails silently (a RuntimeWarning, not an exception), configure ruff and pytest's warning filters to treat it as an error so the bug is caught immediately, not discovered later as a mysteriously-missing side effect.

Use asyncio.create_task() when you need to start work and continue immediately, await directly when you need the result before proceeding

Reaching for create_task() unnecessarily adds complexity for genuinely sequential logic; reaching for direct await when you actually needed concurrency silently serializes work that could have overlapped.

Frequent Bugs

THE BUG

Calling a coroutine function without await inside another async function, silently skipping that coroutine's entire body with only a RuntimeWarning (not an exception) as a hint.

THE FIX

Always await a coroutine call (or explicitly schedule it with create_task/gather if concurrency is intended) — never leave a coroutine-returning call as a bare, unawaited expression statement.

Real-World Examples

Starting a Background Task While Processing a Request

A web request handler needs to log analytics data (which involves a slow network call) without making the user wait for that logging call to complete before receiving their response.

async def handle_request(request):
    # Start logging concurrently, don't block the response on it
    log_task = asyncio.create_task(log_analytics_event(request))

    response_data = await build_response(request)

    # Ensure logging completes before the handler function itself returns,
    # without having made the RESPONSE wait for it
    await log_task
    return response_data

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Writing await some_coroutine() inside a plain (non-async) def function, causing SyntaxError: 'await' outside async function.

# Wrong: await in a non-async function def main(): result = await greet("Ada") # SyntaxError # Correct async def main(): result = await greet("Ada") asyncio.run(main())

The Solution //

await is only valid inside a function defined with async def. If you need to call async code from synchronous code, use asyncio.run() at the appropriate entry point instead.

Lesson Glossary

[01]Coroutine function

A function defined with async def; calling it returns a coroutine object rather than executing its body immediately.

Code Preview
// Coroutine function context

[02]Coroutine object

The inert, not-yet-started representation of a coroutine function call, produced by calling it, requiring await or scheduling to actually run.

Code Preview
// Coroutine object context

[03]await

A keyword that runs a coroutine (or other awaitable), suspending the calling coroutine until it completes, and yields its return value.

Code Preview
// await context

[04]asyncio.create_task()

A function that schedules a coroutine to start running concurrently immediately, returning a Task object that can be awaited later for its result.

Code Preview
// asyncio.create_task() context

Continue Learning