DEV Community

Cover image for Python for AI Engineers: Asyncio, APIs, and What Tutorials Skip
Muhammad Saud
Muhammad Saud

Posted on Originally published at logicstack.org

Python for AI Engineers: Asyncio, APIs, and What Tutorials Skip

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())
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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 tenacity with 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.CancelledError propagates. 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.AsyncClient and 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)