Python for agents (only the parts that matter)

Typing, async, dataclasses, and the standard-library patterns that show up in every agent codebase.

🐍 Module 0 7 min read Not started

Why this matters

Every agentic AI job posting you will ever see lists "strong Python." But you do not need all of Python β€” you need a focused subset that shows up daily in agent code: typing, Pydantic, async/await, decorators, context managers, and clean project structure. This lesson gets you fluent in exactly that subset.

If you already know all of these, skim and run the labs to confirm. If anything looks new, slow down β€” these patterns repeat in every lesson after this.

Learning objectives

After this lesson you will:

  1. Write type-hinted Python that any senior engineer would respect.
  2. Define Pydantic models that LLMs and your code can both trust.
  3. Write async/await code that runs many LLM calls in parallel.
  4. Manage your project with uv (the modern 2026 standard).
  5. Keep secrets out of code with python-dotenv.

1. Type hints (your first 30 minutes of senior-level Python)

Type hints are the single biggest signal of "this person ships production code."

python
def greet(name: str, times: int = 1) -> str:
    return f"Hello {name}! " * times

The arrows and colons are documentation that your editor and tools enforce. They do not change runtime behaviour, but they:

  • Catch bugs in your editor before you run anything.
  • Make refactors safe.
  • Are required by Pydantic, FastAPI, LangGraph, and basically every modern lib.

Common types you will use daily:

python
from typing import Optional, Literal, TypedDict, Annotated

# Optional means "or None"
def find_user(user_id: int) -> Optional[dict]: ...

# Literal locks values to a fixed set (great for routing!)
def route(intent: Literal["billing", "support", "sales"]) -> str: ...

# TypedDict is a dict with known keys (LangGraph state uses these)
class AgentState(TypedDict):
    question: str
    answer: Optional[str]
    steps: list[str]

Run mypy your_file.py or just turn on the Python extension in VS Code and the squiggles will appear.


2. Pydantic β€” the most important library you will touch

Pydantic is how LLM outputs become trustworthy Python objects. It validates, parses, and gives clear errors. Every single agent framework uses it.

python
from pydantic import BaseModel, Field, EmailStr
from typing import Optional

class Customer(BaseModel):
    name: str = Field(..., min_length=2)
    email: EmailStr
    age: int = Field(..., ge=18, le=120)
    notes: Optional[str] = None

# Build one from a dict (like you would from JSON)
c = Customer(name="Asha", email="asha@example.com", age=29)

# Build one from a JSON string
c = Customer.model_validate_json('{"name":"Asha","email":"a@b.com","age":29}')

# Get a JSON schema (this is what we will pass to LLMs!)
print(Customer.model_json_schema())

If the data is bad, Pydantic raises ValidationError with a clean message. That is what powers the instructor library's auto-retry: failed validation β†’ tell the LLM the error β†’ ask it to fix β†’ repeat.

Pydantic patterns you will reuse:

python
from pydantic import field_validator, model_validator

class Order(BaseModel):
    items: list[str]
    total: float

    @field_validator("total")
    @classmethod
    def must_be_positive(cls, v):
        if v <= 0:
            raise ValueError("total must be positive")
        return v

You do not need to memorize Pydantic. You need to know it exists, and that LLM tools rely on it heavily.


3. async/await β€” making 10 slow API calls take the time of one

Without async, calling 10 LLMs sequentially takes 10 Γ— latency. With async, they all happen in parallel and you wait once.

python
import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI()

async def ask(question: str) -> str:
    resp = await client.chat.completions.create(
        model="gpt-4.1-mini",
        messages=[{"role": "user", "content": question}],
    )
    return resp.choices[0].message.content

async def main():
    questions = ["why is the sky blue?", "what is RAG?", "explain async"]
    answers = await asyncio.gather(*(ask(q) for q in questions))
    for a in answers:
        print(a, "\n---")

asyncio.run(main())

Mental model: await says "park this coroutine, let others run while we wait." asyncio.gather runs many in parallel.

You will use await asyncio.gather(...) constantly in agent code that calls multiple tools or LLMs.

Common pitfall: never call time.sleep() inside an async function β€” use await asyncio.sleep(). The first one blocks the entire event loop.


4. Decorators (only the ones agent libraries use)

Two decorators show up over and over:

python
from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_lookup(key: str) -> str:
    # cached after first call
    ...
python
# Tools in LangChain, Pydantic AI, OpenAI Agents SDK all use a @tool decorator
from langchain_core.tools import tool

@tool
def get_stock_price(ticker: str) -> float:
    """Return the latest stock price for the given ticker."""
    return 142.50  # pretend API call

The docstring becomes the description shown to the LLM. The function signature becomes the JSON schema. That is the magic.


5. Project structure for an agent app

This is what every employer expects to see in your GitHub:

my-agent-project/
β”œβ”€β”€ pyproject.toml          # uv-managed dependencies
β”œβ”€β”€ uv.lock                 # locked versions
β”œβ”€β”€ .env                    # secrets (NEVER committed)
β”œβ”€β”€ .env.example            # template (committed)
β”œβ”€β”€ .gitignore
β”œβ”€β”€ README.md
β”œβ”€β”€ src/
β”‚   └── my_agent/
β”‚       β”œβ”€β”€ __init__.py
β”‚       β”œβ”€β”€ agent.py        # the agent graph / loop
β”‚       β”œβ”€β”€ tools.py        # tool definitions
β”‚       β”œβ”€β”€ prompts.py      # prompt templates
β”‚       β”œβ”€β”€ memory.py       # memory adapter
β”‚       └── config.py       # settings (BaseSettings)
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_tools.py
β”‚   └── eval_dataset.json   # golden Q/A pairs
└── docker/
    └── Dockerfile

Set this up with uv (the modern, fast replacement for pip + venv):

powershell
uv init my-agent-project
cd my-agent-project
uv add openai anthropic pydantic python-dotenv langchain langgraph
uv run python -c "import openai; print(openai.__version__)"

uv is 10-100x faster than pip and is now standard in 2026 agent codebases.


6. Secrets management

python
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    openai_api_key: str
    anthropic_api_key: str
    langsmith_api_key: str | None = None

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

settings = Settings()

pydantic-settings reads .env and validates the values exist. If OPENAI_API_KEY is missing, your app refuses to start β€” which is exactly what you want. It is one of those small touches that screams "I have shipped production before."

Add to .gitignore:

.env
.venv/
__pycache__/
*.pyc

Hands-on lab (90 minutes)

Build a tiny CLI tool that:

  1. Reads a list of 5 questions from questions.txt.
  2. Asks GPT-4.1-mini and Claude-3.5-haiku in parallel, both questions at the same time using asyncio.gather.
  3. Validates each answer through a Pydantic model:
    python
    class Answer(BaseModel):
        summary: str = Field(..., max_length=200)
        confidence: Literal["low", "medium", "high"]
  4. Saves results to answers.json.

Acceptance criteria:

  • All secrets loaded from .env.
  • Use uv for dependencies, not pip.
  • Type hints on every function.
  • A pytest test that asserts the JSON has 5 entries and each is valid.
  • Total runtime under the time of one sequential call (proof async works).

Common pitfalls

  1. Mixing sync and async β€” you cannot await inside a regular def, and you cannot call an async function without await. Pick a lane per file.
  2. Blocking the loop β€” requests.get(), time.sleep(), cv2.imread() are sync. Use httpx, asyncio.sleep, asyncio.to_thread(...).
  3. Forgetting model_config β€” Pydantic v2 silently allows extra fields. Add extra="forbid" for OpenAI structured outputs.
  4. Hard-coding API keys β€” every junior does this once. Use .env. Period.
  5. Using pip install in 2026 β€” uv is faster and reproducible. Hiring managers notice the uv.lock.

Self-check (answer these in your own words)

  1. What does Optional[X] translate to at runtime?
  2. Why does Pydantic prefer model_validate_json over json.loads + Model(**)?
  3. When would asyncio.gather not speed things up?
  4. What is the difference between @tool and @lru_cache?
  5. Why is uv better than pip for production projects?

References (read these once)

Sign in to save your progress and earn badges.