Python for agents (only the parts that matter)
Typing, async, dataclasses, and the standard-library patterns that show up in every agent codebase.
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:
- Write type-hinted Python that any senior engineer would respect.
- Define Pydantic models that LLMs and your code can both trust.
- Write
async/awaitcode that runs many LLM calls in parallel. - Manage your project with
uv(the modern 2026 standard). - 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."
def greet(name: str, times: int = 1) -> str:
return f"Hello {name}! " * timesThe 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:
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.
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:
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 vYou 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.
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:
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_lookup(key: str) -> str:
# cached after first call
...# 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 callThe 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/
βββ DockerfileSet this up with uv (the modern, fast replacement for pip + venv):
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
# 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__/
*.pycHands-on lab (90 minutes)
Build a tiny CLI tool that:
- Reads a list of 5 questions from
questions.txt. - Asks GPT-4.1-mini and Claude-3.5-haiku in parallel, both questions at the same time using
asyncio.gather. - Validates each answer through a Pydantic model:python
class Answer(BaseModel): summary: str = Field(..., max_length=200) confidence: Literal["low", "medium", "high"] - Saves results to
answers.json.
Acceptance criteria:
- All secrets loaded from
.env. - Use
uvfor dependencies, not pip. - Type hints on every function.
- A
pytesttest 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
- Mixing sync and async β you cannot
awaitinside a regulardef, and you cannot call an async function withoutawait. Pick a lane per file. - Blocking the loop β
requests.get(),time.sleep(),cv2.imread()are sync. Usehttpx,asyncio.sleep,asyncio.to_thread(...). - Forgetting
model_configβ Pydantic v2 silently allows extra fields. Addextra="forbid"for OpenAI structured outputs. - Hard-coding API keys β every junior does this once. Use
.env. Period. - Using
pip installin 2026 βuvis faster and reproducible. Hiring managers notice theuv.lock.
Self-check (answer these in your own words)
- What does
Optional[X]translate to at runtime? - Why does Pydantic prefer
model_validate_jsonoverjson.loads + Model(**)? - When would
asyncio.gathernot speed things up? - What is the difference between
@tooland@lru_cache? - Why is
uvbetter thanpipfor production projects?
References (read these once)
Sign in to save your progress and earn badges.