Python async def: Syntax and Usage
Learn how to define and run coroutines with python async def, use await, and manage concurrent tasks with asyncio.
python async def requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
What Is an async def Function?
An async def function in Python defines a coroutine. When you call it, it returns a coroutine object rather than executing the body immediately. The body runs only when the coroutine is awaited or scheduled on an event loop. This is the foundation of asynchronous programming in Python.
The Basic Syntax of async def and await
The syntax is straightforward:
async def fetch_data(): return {"status": "ok"}
To run the body, you must await the coroutine or pass it to an event loop. Inside an async function, you can use await to suspend execution until another awaitable completes.
async def main(): result = await fetch_data() print(result)
How Async Functions Run: The Event Loop
When you call an async function, Python creates a coroutine object. The event loop is responsible for scheduling and executing coroutines. The loop runs the coroutine until it hits an await, at which point it yields control back to the loop, allowing other tasks to run. This cooperative multitasking is what enables concurrency without threads.
Running Async Code with asyncio.run
The simplest way to run a top-level coroutine is asyncio.run(). It creates a new event loop, runs the coroutine, and closes the loop.
import asyncio async def main(): print("Hello") await asyncio.sleep(1) print("World") asyncio.run(main())
This is the standard entry point for most async programs.
Achieving Concurrency with Tasks and gather
To run multiple async functions concurrently, you create tasks with asyncio.create_task() or use asyncio.gather().
async def task_a(): await asyncio.sleep(1) return "A" async def task_b(): await asyncio.sleep(2) return "B" async def main(): results = await asyncio.gather(task_a(), task_b()) print(results) asyncio.run(main())
This runs both tasks concurrently, reducing total wait time.
Common Pitfalls When Using async def
A frequent mistake is calling an async function without awaiting it, which returns a coroutine object and never runs the body. Another is using blocking I/O calls like time.sleep() inside an async function, which blocks the entire event loop. Use await asyncio.sleep() instead. Also, be careful with shared mutable state across tasks, as concurrency can introduce race conditions.
When to Use async def vs Regular def
Use async def for I/O-bound operations such as network requests, file I/O, or database calls that benefit from concurrency. For CPU-bound work, async does not speed up execution; it may even add overhead. Regular functions are simpler and appropriate for synchronous logic. The choice depends on the workload and whether you need to manage multiple I/O operations concurrently.
Performance and Operational Considerations
Async code reduces overhead compared to thread-based concurrency for many I/O-bound tasks, but it requires careful design. The event loop is single-threaded, so any CPU-bound code inside an async function will block all other tasks. In production, you need to handle cancellation, timeouts, and proper resource cleanup. Use asyncio.timeout or asyncio.wait_for to limit execution time.