Module 0.8

Python Orientation for Beginners

1–2 hr · Read + practice

You don’t need to be a Python expert to ship a working AI agent. You need to read agent code with confidence — see a function decorated with async def, an import from pydantic, a type hint like messages: list[dict[str, str]], and not freeze. This tutorial (about 90 minutes including practice) gets you there.

You’ve already worked through the JavaScript and TypeScript references for the React side of the program. Python is going to feel both familiar and different. The control flow (if, for, while), the data types (numbers, strings, booleans, lists, dicts), and the rough idea of functions and classes will all transfer. What’s different: whitespace matters (no curly braces), there’s a package manager called uv you’ll meet for the first time, every project lives inside a virtual environment, and the type system is optional but standardized.

We use Python in this program for the agent backend (LangGraph, Pydantic AI, FastAPI) because the AI ecosystem is overwhelmingly Python-first. You’ll write or modify a few dozen lines a week — not whole frameworks.


1. Install Python with uv

The traditional way to install Python (downloading from python.org, fighting with system Python and PATH) is messy. The 2026 way is uv — a single tool that installs Python, manages virtual environments, and resolves dependencies. It’s written in Rust, runs 10–100× faster than the old tools, and replaces a tangle of legacy commands.

If you completed the macOS Installation Guide (the Setup step before Module 0.4), uv is already on your PATH. Verify:

$ uv --version
uv 0.x.x

If the installation guide ran successfully, Python 3.13 is already installed. Confirm:

$ uv python list --only-installed
cpython-3.13.x-macos-...   (installed)

That’s it. You now have Python 3.13 available without touching system Python.

Why uv instead of pip and venv? Three reasons: speed, a single tool that handles versions and packages, and a lockfile (uv.lock) so every machine running the project gets identical dependencies. The legacy pip and venv commands still exist; you’ll see them in old tutorials. Use uv.


2. Create a project and virtual environment

A virtual environment (“venv”) is a folder containing a project’s own copy of Python and every dependency. Every project has its own venv so they don’t conflict. This is the equivalent of node_modules in JavaScript — the venv lives at .venv/ inside your project.

Create a fresh project:

$ mkdir python-practice
$ cd python-practice
$ uv init

You’ll see new files:

Add a dependency:

$ uv add pydantic

This creates .venv/, installs Pydantic into it, and updates pyproject.toml plus uv.lock.

Run the stub script:

$ uv run main.py
Hello from python-practice!

uv run is the launcher. It activates the venv automatically before running the script. You won’t need to remember to “activate the venv” — uv run does it for you. (Older tutorials will tell you to source .venv/bin/activate. With uv, you don’t have to.)


3. Whitespace and the basics

Python uses indentation instead of curly braces to define blocks. Every level inside a function, loop, or if is indented one level deeper (4 spaces is the convention).

def greet(name):
    if name:
        print(f"Hello, {name}")
    else:
        print("Hello, anonymous")

greet("Alex")  # Hello, Alex

If you mix tabs and spaces, Python will refuse to run. VS Code handles indentation automatically — trust the auto-format.

The basic types and structures map to JavaScript closely:

age = 25                              # int
name = "Alex"                         # str
is_host = True                        # bool (capital T/F!)
languages = ["Python", "TypeScript"]  # list — like a JS array
person = {"name": "Alex", "age": 25}  # dict — like a JS object

Loops and conditionals look like JavaScript with the braces stripped:

for lang in languages:
    print(lang)

if age >= 18:
    print("Adult")
elif age >= 13:
    print("Teen")
else:
    print("Child")

Why this matters: Most agent code you’ll read is a sequence of if/for/def blocks. Once the indentation rules click, Python feels less foreign than TypeScript did the first time.


4. Type hints

Python is dynamically typed by default — variables don’t have types until you assign them. But type hints (standard since Python 3.5) let you annotate types the same way TypeScript does. The Python interpreter ignores them at runtime; tools like mypy and your IDE use them to catch bugs and give autocomplete.

def greet(name: str) -> str:
    return f"Hello, {name}"

age: int = 25
languages: list[str] = ["Python", "TypeScript"]
person: dict[str, str | int] = {"name": "Alex", "age": 25}

The pattern is identical to TypeScript: write the name, a colon, then the type. The type names are different. Cheat sheet:

TypeScriptPython
stringstr
numberint or float
booleanbool
Array<T> / T[]list[T]
Record<K, V>dict[K, V]
T | undefinedT | None
void (return)None

You’ll see type hints everywhere in agent code. Embrace them.


5. f-strings

An f-string is a Python string with embedded expressions. The f prefix activates substitution inside {} braces.

name = "Alex"
score = 87.5
print(f"{name} scored {score}")        # Alex scored 87.5
print(f"Score rounded: {score:.0f}")   # Score rounded: 88

f-strings are Python’s equivalent of JavaScript template literals (the backtick ${} syntax). You’ll use them constantly for prompts, log lines, SQL fragments, and assertion messages.


6. Imports

import json
from datetime import datetime
from pydantic import BaseModel

Both styles appear in agent code. from pydantic import BaseModel and from openai import AsyncOpenAI are typical.

To install a dependency someone else’s code references:

$ uv add openai

That’s the entire install dance for that package — no requirements.txt editing, no pip freeze.


7. Async / await

Most agent code is asynchronous — it makes network calls (to OpenAI, Anthropic, databases) and shouldn’t block while it waits. Python’s async/await works almost identically to JavaScript’s.

import asyncio

async def fetch_user(user_id: str) -> dict:
    # Pretend this hits a real API
    await asyncio.sleep(0.1)
    return {"id": user_id, "name": "Alex"}

async def main():
    user = await fetch_user("u-123")
    print(user)

asyncio.run(main())

The rules:

You can only use await inside an async def function. The one place this gets relaxed is FastAPI route handlers — the framework provides the async runtime, so you can await freely.

Why this matters: Every LLM call (await client.messages.create(...)), every database query (await db.fetch(...)), every HTTP request to an external service is awaited. Reading agent code is mostly reading lines with await in them.


8. Pydantic models

Pydantic is the library that defines the shape of structured data — function arguments, API responses, agent outputs. It’s how Python agents do what TypeScript does with interface. Every Python agent framework you’ll touch (Pydantic AI, LangGraph, FastAPI) is built on Pydantic.

from pydantic import BaseModel

class User(BaseModel):
    id: str
    name: str
    age: int
    email: str | None = None  # optional, defaults to None

# Create one from a dict (e.g., parsed JSON):
data = {"id": "u-123", "name": "Alex", "age": 25}
user = User(**data)

# Pydantic validates — wrong types raise an error:
bad = User(id="u-456", name="Sam", age="twenty-five")  # ValidationError

# Convert back to a dict or JSON string:
user.model_dump()       # {"id": "u-123", "name": "Alex", ...}
user.model_dump_json()  # '{"id":"u-123","name":"Alex",...}'

A BaseModel subclass is your guarantee that data going in or out of an agent is structured. When you see code like:

class StudyPlan(BaseModel):
    topic: str
    target_depth: int
    target_date: datetime
    daily_slots: list[str]

That’s a contract — anywhere a StudyPlan is expected, the four fields must be present and the types must match. The LLM is prompted to return JSON matching that shape, and Pydantic validates it before your code uses it. Garbage in is rejected at the door.


9. Putting it together — a sample agent excerpt

Here’s a representative slice of agent code. After this tutorial you should be able to read every line.

import asyncio
from pydantic import BaseModel
from openai import AsyncOpenAI

client = AsyncOpenAI()

class StudyPlan(BaseModel):
    topic: str
    target_depth: int
    daily_slots: list[str]

async def generate_plan(topic: str) -> StudyPlan:
    response = await client.responses.create(
        model="gpt-5",
        instructions="Return a JSON StudyPlan: topic, target_depth (1-5), and 3-5 daily_slots.",
        input=f"Topic: {topic}",
    )
    return StudyPlan.model_validate_json(response.output_text)

async def main():
    plan = await generate_plan("Linear algebra fundamentals")
    print(f"Plan for {plan.topic}{len(plan.daily_slots)} slots")

asyncio.run(main())

Walk through it line by line:

  1. Import the async OpenAI client and our Pydantic model class.
  2. Construct a module-level client.
  3. Define StudyPlan — the schema we want the LLM to return.
  4. async def generate_plan — the function is async because it awaits the OpenAI call.
  5. await client.responses.create(...) — the actual LLM call. It pauses here until the model responds.
  6. StudyPlan.model_validate_json(...) — parse the response string into a typed object. If the LLM returned something that doesn’t match the schema, this raises immediately.
  7. asyncio.run(main()) — the top-level launcher.

Every agent assignment in the program will be a variation on this pattern.


10. Practice exercises

Create python-practice/ per Section 2. Then try these one at a time. Type them; don’t paste — muscle memory matters.

1. A typed function. Create greeter.py:

def greet(name: str, exclamation: bool = False) -> str:
    suffix = "!" if exclamation else "."
    return f"Hello, {name}{suffix}"

print(greet("Alex"))
print(greet("Sam", exclamation=True))

Run with uv run greeter.py. You should see two greetings.

2. A Pydantic model. Make sure Pydantic is installed: uv add pydantic. Then create recipe.py:

from pydantic import BaseModel

class Recipe(BaseModel):
    title: str
    servings: int
    ingredients: list[str]

dinner = Recipe(title="Pasta", servings=2, ingredients=["spaghetti", "tomato", "basil"])
print(dinner.model_dump_json(indent=2))

Run it. You should see a JSON-formatted recipe.

3. Break Pydantic on purpose. Add this at the bottom of recipe.py:

broken = Recipe(title="Pizza", servings="two", ingredients=["dough"])

Run it. Read the ValidationError carefully. That is what Pydantic does for your agent.

4. An async function with a delay. Create async_demo.py:

import asyncio

async def slow_double(n: int) -> int:
    await asyncio.sleep(0.5)
    return n * 2

async def main():
    result = await slow_double(21)
    print(f"Result: {result}")

asyncio.run(main())

Run it. The half-second pause is the await in action.


11. Cheat sheet — keep this open while you code

ConceptSyntax
Install Pythonuv python install 3.13
Create projectuv init
Add dependencyuv add <package>
Run scriptuv run <file.py>
Type hint variableage: int = 25
Type hint functiondef f(x: str) -> int: ...
Optional typename: str | None = None
List typeitems: list[str]
Dict typedata: dict[str, int]
f-stringf"Hello, {name}"
Async functionasync def fetch(): ...
Await callresult = await fetch()
Top-level asyncasyncio.run(main())
Pydantic modelclass X(BaseModel): name: str
Validate from JSONX.model_validate_json(s)
Dump to dictobj.model_dump()

What’s next

You’re now Python-literate enough to read every line of the agent code you’ll meet in Weeks 3–8. You don’t need to memorize everything — keep the cheat sheet open, and the syntax will become reflexive after the first lab.

When you’re ready, post the output of your Recipe model from Exercise 2 in #wins on Discord with the message “Module 0.8 complete.” See you in the next module.