# Async and Await in Python: A Primer for AI Agent Code (2026)

> An async and await in Python primer for reading AI agent code: what async def, await, and asyncio.run mean, and the two mistakes beginners always hit.

*Source: https://www.infowok.com/async-await-python-primer/ · Sukhveer Kaur · Published June 22, 2026*

---

Open almost any AI agent example and the first line of real code is `async def`. Then every interesting call has an `await` in front of it, and the whole thing ends with `asyncio.run(main())`. If that syntax makes a simple tutorial feel advanced, you are not missing anything deep — you are missing two keywords that nobody stopped to explain. This **async and await in Python** primer explains them, so agent code reads like plain English.

You do not need to *master* async to build agents in 2026. You need to *read* it, because the LLM SDKs and frameworks you will use ship async-first examples. By the end of this short read you will know what `async def` and `await` mean, when they help, and the two mistakes that catch every beginner.

<Prerequisites>

- You can read a basic Python function and run a `.py` file — new to that? The [Python for AI agents primer](/ai-agents-from-scratch-python-part-0/) covers the basics
- That's the whole list — this primer assumes no async experience at all

</Prerequisites>

<KeyTakeaways>

- **`async def` makes a coroutine:** calling it doesn't run the body, it hands you a coroutine you have to `await`.
- **`await` means "pause here until this finishes"** and lets the event loop run other work while you wait — that's concurrency, not parallelism.
- **`asyncio.run(main())` is the on-ramp:** it starts the event loop and runs your top-level async function.
- **The two beginner traps** are forgetting `await` (nothing runs) and calling slow blocking code inside async (everything freezes).

</KeyTakeaways>

## What async and await actually mean: waiting without freezing

Picture a barista taking an order, starting the espresso machine, and — instead of standing there watching it drip — taking the next customer's order while the machine works. Same person, one counter, but no idle waiting. That is **async**: one thread that switches to other work whenever it would otherwise be stuck waiting.

The thing it waits on is almost always **I/O** (input/output) — a network request, a database query, an LLM API call. These are slow not because your computer is busy, but because it's waiting on someone else's server. Async lets a single program fire off one API call and start another while the first is still in flight. **Async is about not wasting time waiting, not about doing two calculations at once.** That second thing is parallelism, and it's a different tool (threads and processes).

This is exactly why agent code is async-first: an agent's whole life is waiting on model and tool calls over the network. Async is the natural fit.

## `async def` and `await`: the only two keywords

Here are the two pieces of syntax you'll read constantly. A function defined with **`async def`** is a *coroutine*. The surprise for newcomers: calling it does **not** run the body.

```python
import asyncio

async def get_weather(city: str) -> str:
    await asyncio.sleep(1)          # pretend this is a slow API call
    return f"It's sunny in {city}."

result = get_weather("Pune")        # ⚠️ does NOT run — result is a coroutine object
```

To actually run a coroutine, you **`await`** it from inside another async function. `await` means "run this, and pause me until it returns — and while I'm paused, let other work happen."

```python
async def main():
    report = await get_weather("Pune")   # runs it, waits for the result
    print(report)
```

<Callout type="key" title="The one-sentence version">
`async def` defines work that can be paused; `await` is where it pauses and resumes. Every other piece of asyncio is built on those two ideas.
</Callout>

## Running async code: `asyncio.run`

You can only `await` inside an async function — so how does the *first* one start? That's what [**`asyncio.run()`**](https://docs.python.org/3/library/asyncio-runner.html#asyncio.run) is for. It starts the **event loop** (the scheduler that runs coroutines and switches between them) and runs your top-level coroutine to completion.

```python
import asyncio

async def main():
    print(await get_weather("Delhi"))

asyncio.run(main())     # the single entry point that boots the loop
```

The rule of thumb: **`asyncio.run(main())` appears exactly once, at the very bottom of the script.** Everything async flows out of that one call.

## Doing things at once: gather and TaskGroup

The real payoff comes when you await several slow things *together* instead of one after another. The modern, safe way is **`asyncio.TaskGroup`** (Python 3.11+), which starts tasks and waits for all of them — and cancels the rest if one fails.

```python
async def main():
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(get_weather("Pune"))
        t2 = tg.create_task(get_weather("Delhi"))
    print(t1.result(), t2.result())   # both ran concurrently
```

Run two one-second calls sequentially and it takes two seconds; run them in a TaskGroup and it takes about one. For an agent calling several tools at once, that difference is the whole point.

<Callout type="tip" title="TaskGroup over gather in 2026">
Older tutorials use `asyncio.gather(...)`. It still works, but `TaskGroup` has cleaner error handling — if one task fails, it cancels the others instead of leaving them running. Prefer it for new code.
</Callout>

## The two mistakes that bite beginners

Almost every async bug a beginner hits is one of these two, so learn to spot them now.

**Forgetting `await`.** Call a coroutine without `await` and the body never runs — you get a coroutine object and a "coroutine was never awaited" warning, with no error and no result. If an async call seems to do nothing, check for a missing `await` before anything else.

**Blocking the loop.** The event loop only switches at an `await`. Drop a slow *synchronous* call into an async function — `time.sleep(5)`, `requests.get(...)`, a heavy CPU loop — and the whole program freezes, because nothing yields control back to the loop.

<Callout type="warning" title="Use the async version of slow calls">
Inside async code, use `await asyncio.sleep()` not `time.sleep()`, and an async HTTP client like `httpx.AsyncClient` not `requests`. A blocking call in a coroutine defeats the entire purpose of async — one frozen call stalls every other task.
</Callout>

## When you don't actually need async

Async is not free — it adds keywords, an event loop, and a class of bugs. **If your script makes one API call and exits, plain synchronous code is simpler and just as fast.** Reach for async when you're doing many I/O operations and waiting is the bottleneck: several tool calls at once, a web server handling many requests, a batch of documents to fetch. That's why agent frameworks default to it — but a small one-shot script doesn't need it.

## Quick recap

Everything above, in five lines:

- **`async def`** defines a coroutine; calling it returns an object you must `await`.
- **`await`** runs a coroutine and pauses until it's done, freeing the loop for other work.
- **`asyncio.run(main())`** boots the event loop — once, at the bottom of the script.
- **`TaskGroup`** runs several coroutines concurrently and fails safely.
- **Watch for** a missing `await` (nothing runs) and blocking calls (everything freezes).

## Frequently Asked Questions

**What do async and await actually do?** `async def` makes a coroutine that doesn't run until awaited; `await` runs it and pauses the caller until it returns, letting the event loop do other work meanwhile.

**Do I need async to build AI agents?** You need to *read* it, because frameworks ship async examples. Writing your own comes later, when you actually need concurrency.

**Async or threads?** Async (single thread, switches at `await`) suits I/O-bound work like API calls; threads suit CPU-bound work. Agents are I/O-bound, so async fits.

**Why doesn't my coroutine run?** You called it without `await`. Add `await` inside an async function, or run the top one with `asyncio.run()`.

## Conclusion

Async stops being intimidating the moment you read async and await as one idea: `async def` marks work that can pause, `await` is where it pauses and resumes, and `asyncio.run` starts the whole thing. That's enough to follow any agent quickstart line by line — which is the real goal, since you'll read far more async code than you write at first.

**What's the first piece of async agent code you want to actually understand — an LLM call, a tool, a whole agent loop?** Tell me in the comments.

<NextSteps>

- **Need the Python basics first?** The [Python for AI agents primer](/ai-agents-from-scratch-python-part-0/) covers dicts, functions, and loops.
- **Ready to use it?** [Call an LLM in Python](/ai-agents-from-scratch-python-part-1/) is your first real async-friendly agent code.
- **Connecting to real tools?** The [MCP client tutorial](/build-agentic-ai-app-python-part-5/) leans on `async`/`await` throughout.

</NextSteps>
