Python for AI Engineers: Asyncio, APIs, and What Tutorials Skip
Most Python tutorials teach you loops, classes, and list comprehensions. None of that is what slows down an AI engineer. What slows you down is waiting: waiting on an LLM API, waiting on a vector database, waiting on a tool call inside an agent loop. This part of the series is about the Python that actually matters when you build AI systems: async programming, API patterns, and the production habits tutorials never mention.
Part 2 of the AI Engineer Series. Part 1 mapped the full roadmap from 390 real job postings. This part covers the Python foundation. Part 3 goes inside the model itself.
Why AI Engineering Is Async by Nature
Think about what an AI system actually does all day. Your agent calls an LLM API and waits 2 seconds. It searches a vector database and waits 80 milliseconds. It calls three tools and waits on each. Then it calls the model again.
None of this is computation. It is waiting on the network. In Part 1 we saw Python is required in 59% of AI engineer postings, and this is the specific Python they mean: the ability to run hundreds of these waiting operations at the same time instead of one after another.
Synchronous code turns waiting into waste. If your agent makes 10 tool calls that each take 1 second, sequential code takes 10 seconds. Concurrent code takes about 1 second. That difference is the whole game in AI engineering.
The Benchmark That Ends the Debate
A head-to-head test fired 100 HTTP requests, each with 200 milliseconds of latency, at a mock service using three strategies (dev.to, 2026):
| Strategy | Total time | Notes |
|---|---|---|
| Sequential sync | 20.3s | CPU nearly idle, pure waiting |
| 100 threads | 8.5s | CPU spiked to 90%, thread-switching overhead |
| 30 threads (tuned) | 2.1s | Better, but fragile as thread count grows |
| asyncio + aiohttp | 0.45-0.6s | CPU under 15%, memory flat |
Asyncio was roughly 35 times faster than sequential and 14 times faster than naive threading. A separate benchmark measured memory directly: 10,000 asyncio tasks used 84 MB and started in 0.14 seconds, while just 1,000 OS threads consumed 1.42 GB and took 2.85 seconds to set up (RiddleTech, 2026). Asyncio uses 94% less memory per concurrent operation.
The lesson is not that threads are bad. It is that for I/O-bound work, which is nearly all AI engineering work, asyncio is in a different league.
Pattern 1: Parallel LLM Calls with gather
The most common AI engineering pattern is also the simplest: fire several independent LLM calls at once and collect the results.
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def classify(text: str) -> str:
resp = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"Classify: {text}"}],
max_tokens=20,
)
return resp.choices[0].message.content
async def main():
texts = ["refund request", "bug report", "pricing question"] * 20
# 60 calls run concurrently, not one after another
results = await asyncio.gather(*[classify(t) for t in texts])
return results
asyncio.run(main())
Two things tutorials skip here. First, use the provider's async client (AsyncOpenAI, not OpenAI). The sync client blocks the event loop and silently turns your concurrent code back into sequential code. Second, on Python 3.11+, prefer asyncio.TaskGroup over gather for structured concurrency: it cancels sibling tasks automatically if one fails, which is exactly what you want when an agent step goes wrong.
Pattern 2: Semaphores for Rate Limits
Every LLM provider caps how many requests you can send per minute. Firing 500 concurrent calls gets you rate-limited or, worse, a surprise bill. A semaphore caps concurrency without changing your code structure:
semaphore = asyncio.Semaphore(10) # max 10 calls in flight
async def classify_limited(text: str) -> str:
async with semaphore:
return await classify(text)
Wrap every provider call in a semaphore. Start with 5 to 10 concurrent requests and raise it only when you see headroom. This one pattern prevents most of the rate-limit errors beginners hit.
Pattern 3: Timeouts on Everything
An LLM call can hang. A tool server can stall. Without a timeout, one stuck call freezes your whole pipeline. asyncio.wait_for sets a hard ceiling:
try:
result = await asyncio.wait_for(classify(text), timeout=30)
except asyncio.TimeoutError:
result = "TIMEOUT" # fall back, retry, or log and move on
Set a timeout on every external call: LLM APIs, vector DB queries, tool executions. Timeouts are the cheapest reliability upgrade in AI engineering.
Pattern 4: Never Block the Event Loop
This is the number one asyncio bug in AI code. The event loop is a single thread. Any blocking call inside it, like requests.get(), time.sleep(), or a heavy PDF parse, freezes everything: all your concurrent tasks, your FastAPI server, your streaming responses. Tutorials rarely say this loudly enough.
The fix is asyncio.to_thread, which runs blocking code in a thread pool without freezing the loop:
import asyncio
import requests # blocking library
async def fetch_docs(url: str) -> str:
# runs in a thread, loop stays free
resp = await asyncio.to_thread(requests.get, url, timeout=10)
return resp.text
Rule of thumb: async libraries for I/O (httpx, aiohttp, provider async clients), to_thread for blocking libraries you cannot avoid, and a process pool for CPU-heavy work like embedding batch math. If your async code is slower than expected, a blocking call hiding in the loop is the first suspect. For tool code you do not fully trust, sandboxing matters even more than threads; see our guide to sandboxing AI agents.
One free speedup: uvloop is a drop-in replacement for the default event loop that makes asyncio 2 to 4 times faster for network I/O, with no code changes. Install it and set the event loop policy.
FastAPI: Streaming LLM Output with SSE
Your agent needs a front door, and that front door is usually FastAPI. The pattern that matters most: streaming tokens to the browser as they are generated, using Server-Sent Events (SSE). Users see the first word in under a second instead of staring at a spinner for 10 seconds.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
app = FastAPI()
client = AsyncOpenAI()
@app.get("/chat")
async def chat(q: str):
async def generate():
stream = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": q}],
stream=True,
)
async for chunk in stream:
token = chunk.choices[0].delta.content
if token:
yield f"data: {token}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
SSE beats WebSockets for this job: it is plain HTTP, it reconnects automatically, and it flows through proxies and load balancers without special configuration. Streaming has one trade-off to know: you cannot post-process a response you have already sent, so validate and filter before you stream, not after.
Pydantic: Your Contract with the Model
LLMs return text. Your code needs data. Pydantic turns the model's output into a validated Python object, and it is the backbone of reliable AI pipelines.
All three major providers now support constrained generation to your JSON schema at decode time: OpenAI via response_format with json_schema and strict: true, Anthropic via output_config.format, and Gemini via responseMimeType: "application/json" plus responseSchema. There is no surcharge; the schema bills as normal input tokens (tokens.bd, 2026). In LangChain this is one call: llm.with_structured_output(MyModel).
But structured outputs do not remove validation. Two failure modes survive the schema guarantee:
-
Safety refusals. Anthropic returns
stop_reason: "refusal"with HTTP 200, billed normally, and the output may not match your schema. Check the stop reason before you parse. -
Token truncation. If the response hits
max_tokens, you get cut-off JSON. Size your output budget for the largest plausible answer.
So the pattern is: ask for structured output, then validate with Pydantic anyway. Pydantic also powers your FastAPI layer: a response_model filters what you send back (no leaking internal fields), and bad requests fail with an automatic 422 instead of crashing your handler. If you are choosing between dataclasses and Pydantic for this role, our benchmarked comparison measured the trade-offs directly.
What Tutorials Skip: The Production Checklist
The patterns above get you working code. These habits get you production code:
-
Retry with backoff. Providers fail. Wrap calls in
tenacitywith exponential backoff and a retry cap. Never retry forever. - Log latency and tokens per call. Every API call costs money and time. A one-line log of model, tokens, and milliseconds per call is the foundation of cost control. Our LLM cost guide measured five techniques that cut real bills.
- Cache aggressively. Identical prompts should never hit the API twice. Use provider prompt caching plus an application-level cache for repeated queries.
-
Handle cancellation gracefully. When a user disconnects mid-stream,
asyncio.CancelledErrorpropagates. Clean up, do not swallow it silently. - Secrets from the environment. API keys in code get committed to git. Use environment variables, always.
-
One shared HTTP client. Create one
httpx.AsyncClientand reuse it. A new client per request throws away connection pooling and adds handshake latency to every call.
Your Minimal Async AI Toolkit
If you take one thing from this part, take this stack: asyncio with TaskGroup, httpx for HTTP, the provider async SDKs, asyncio.Semaphore for rate limits, asyncio.wait_for for timeouts, tenacity for retries, FastAPI with SSE for serving, and Pydantic for every boundary where text becomes data.
This is the Python that 59% of AI engineer job postings are really asking for. Not syntax. Concurrency, APIs, and reliability under real load.
What is Next in This Series
- Part 3: How LLMs Actually Work: Tokens, Context, and Temperature
- Part 4: RAG from Zero: Embeddings, Vector DBs, and Retrieval
- Part 5: Evals: The Skill 56% of Jobs Want
- Part 6: Building Your First AI Agent with LangGraph
- Part 7: MCP: Connecting Agents to Real Tools Safely
- Part 8: LLMOps: Shipping AI to Production
Part 3 opens the black box: what tokens really are, why context windows behave the way they do, and what temperature actually controls. That is the foundation every RAG and agent decision rests on.
Originally published on LogicStack. Run the code examples right in your browser with our free interactive Python and SQL playgrounds — no setup, no sign-up.
Top comments (0)