Study artifact · reconstructed from the session recording

Python, the Right Way — Data Classes, Decorators & Pydantic

A “refresher” on paper, a foundation in practice. The instructor quietly took an AI agent apart into its plain-Python pieces — so the framework weeks later feel like nothing more than imports.

0 — SESSION AT A GLANCE

What this class was really about

On the surface, a Python refresher. Underneath, the instructor was disassembling an AI agent into its plain-Python parts, so that when LangChain/LangGraph arrive in a few weeks, “nothing feels like magic.”

●Core idea — the mental model to keep

An agent is not exotic. It is a class holding some state, calling functions (some wrapped by decorators), talking to services over APIs that return JSON, guarded by error handling, with its inputs and outputs locked down by Pydantic. Master those five ideas in vanilla Python and every framework later is “just imports.” The two-week goal: build an agent (LLM + tools + memory) using only Python, no framework.

Environment Git Bash · uv · VS Code list / dict / JSONhow data is shaped Classes & OOPblueprint + state Decoratorswrap behaviour Error handlingtry / except / finally API / AI callrequest → JSON @dataclassstructured state Pydanticvalidate in / outthe safety layer
The five Python primitives on the left are the raw material; on the right they assemble into the parts of an agent, all wrapped by Pydantic's validation.

Where this sits in the course

The instructor teaches from first principles — concepts before frameworks. This is Phase 0, deliberately placed before agents.

Now (Day 2)Next class~By class 12After
Python foundation: classes, decorators, errors, types AI basics (LLM, tokens, context window) + Pydantic data validation + a real AI call An agent (LLM + tools + memory) in plain Python, no framework LangChain / LangGraph / CrewAI, then real projects, RAG, deploy & agent-ops
⌗How to read this artifact

Coloured callouts carry meaning even in grayscale (each is labelled): Definition, Core idea, Rule, Instructor insight, Tip, Watch out, Example, Extra context. Code blocks are marked ● exact or ◐ reconstructed — the instructor typed while talking, so recovered code is a clean, runnable version of what was narrated, not a verbatim capture.

Git Bashuvlist / dict / JSON classes & self@dataclasstry/except/finally decoratorsrequestsPydantic BaseModel
Topic 1
SETTING UP A REAL DEVELOPER TERMINAL

A Linux-style terminal on Windows

Several students hit errors on Windows. The instructor booted a fresh Windows install running as a VM on top of their Mac to reproduce the exact problem live — and used that VM to teach the “environment / container” idea in passing.

The container / environment intuition

Windows ran as a container on an environment inside macOS. Nothing on the Mac was available inside it — it started blank. The recurring question that motivates reproducible environments: if I set up the exact versions here and hand them to you, won't it just work on your machine too?

Host · macOS (the Mac) Zoom, browser, your real files live out here Guest · Windows VM (a fresh container) Starts empty · nothing from the host is inside · clipboard doesn't cross install VS Code install Git Bash install Python + uv
Anything copied inside the container stays inside; Zoom runs on the host outside it. That boundary is the “environment.”

Three ways to type commands on Windows

TerminalWhat it isRuns shell / Linux commands?
CMDClassic Command PromptNo — sh is “not recognized”
PowerShellNewer Windows shellMostly no (doesn't recognise sh)
Git BashLinux-style shell shipped with Git for WindowsYes — use this
✦Instructor insight — why this is non-negotiable

“99% of the time development happens on a Linux/Unix server. You will not find a developer who can't run a Linux command.” If you can't run shell commands locally, you're helpless the moment you touch a cloud box — so this is table stakes, not polish.

The fix, step by step

  1. Install VS Code and Git for Windows (Git Bash). During Git's installer, accept “add a Git Bash profile to Windows Terminal” and enable Git from the command line.
  2. Latest Windows 11 already ships Windows Terminal — you don't install it. Confirm your build with winver.
  3. Open Git Bash; now the standard install commands work.
  4. In VS Code, open the terminal and pick the Git Bash profile — never PowerShell. “95% of what I do is in the terminal.”
git-bash · commands shown in class
winver                                            # check Windows build/version
curl -LsSf https://astral.sh/uv/install.sh | sh   # install uv — GIT BASH, not CMD/PowerShell
uv --version

uv python install                 # let uv manage a Python interpreter
uv init my_first_project          # scaffold main.py, pyproject.toml, .python-version
cd my_first_project
uv add requests                   # add a dependency (+ writes uv.lock)
uv pip install -r requirements.txt
source .venv/Scripts/activate     # activate venv (Windows / Git Bash)
# source .venv/bin/activate       # activate venv (macOS / Linux)
uv run main.py                    # run a file inside the managed env
▲Watch out
  • Typing python on fresh Windows opens the Microsoft Store (an app-execution alias stub) because no interpreter is installed. Letting uv manage Python removes the surprise.
  • The uv install script uses sh — run it in Git Bash, or it fails in CMD/PowerShell.
  • Activation path differs by OS: .venv/Scripts/activate on Windows vs .venv/bin/activate on macOS/Linux. (The instructor hit this live and corrected it.)
▷Example — exact versions matter

On the VM, uv pulled Python 3.14; the instructor's Mac was on 3.13. Same commands, reproducible setup — that difference is exactly why pinning versions and shipping a lockfile matters.

project virtual env (.venv) dependencies pyproject.toml uv.lock reproducibleinstall
The chain uv ties together so your machine and a teammate's produce the same install.
⬦Checkpoint

You've got it if a new terminal opens a Git Bash prompt, uv --version works, and uv run main.py executes. If not, reinstall Git Bash + VS Code and reopen the terminal. Rule going forward: never open PowerShell; always Git Bash.

Topic 2
THE DATA SHAPES EVERYTHING STANDS ON

Python & JSON refresher

A fast pass over the primitives, run in a notebook so outputs are visible. “I'm not teaching, I'm refreshing” — but the parts that bite people later were flagged deliberately.

Variables, types & f-strings

reconstructed · python
city = "Tokyo"
temperature = 21.5
is_raining = False

print(type(city))         # <class 'str'>
print(type(temperature))  # <class 'float'>
print(type(is_raining))   # <class 'bool'>
print(f"It is {temperature}°C in {city}.")   # f-string interpolation

Lists & negative indexing

reconstructed · python
cities = ["Tokyo", "Paris", "London"]
cities.append("Berlin")
print(cities[0])    # Tokyo  (first)
print(cities[-1])   # Berlin (last)
mixed = [1, "two", 3.0, True]   # a list can hold mixed types — no error
Student questionWhat's the difference between an array and a list?
Instructor clarificationA list holds mixed types and grows freely; a classic array is fixed-size and single-type. “Python has no array by default” the way it has lists.
⌗Extra context (editor)

Precisely: Python does ship an array module (from array import array) for single-type arrays, and NumPy is the real-world choice for numeric work. The aside that Python “is transformed to C” refers loosely to CPython, the reference interpreter written in C — your Python isn't compiled to C source. For this course, lists are all you need.

JSON is a dictionary (for this course)

JSON is how information travels between services, and it's just key–value pairs. Values can themselves be lists or nested dicts.

◆Definition

JSON = a text format of key–value pairs for exchanging data. In this course, treat dict == JSON: a Python dictionary is the in-memory shape of the same thing.

reconstructed · python
person = {
    "name": "Alice",          # key : value
    "age": 25,
    "skills": ["Python", "AI", "SQL"],  # a LIST of values
    "address": {"city": "Mumbai", "pin": "400001"}  # a nested dict / JSON
}
person {} "name": "Alice" "age": 25 "skills": [ ] "address": { } 3 items: Python, AI, SQL 2 keys: city, pin
A list holds ordered items; a nested dict is a complete JSON in its own right. “skills” is an array; “address” is a dictionary.

Reading values safely: .get() vs []

Real APIs return JSON, and the key you want may be missing. Demoed against the free Cat Facts API (returns {"fact": …, "length": …}).

reconstructed · python
reading = {"city": "Tokyo", "temp": 21.5}
print(reading["humidity"])       # 💥 KeyError — crashes
print(reading.get("humidity"))    # None — safe, no crash

for key, value in reading.items():
    print(key, "->", value)
>>> city -> Tokyo  ·  temp -> 21.5
▸Rule to remember

Prefer dict.get("key") when a key might be absent — it returns None instead of raising KeyError. This is exactly the kind of defensive read you want around unpredictable API/AI responses.

⌗Extra context — JSON ≠ dict, strictly

The dict == JSON shortcut is fine for the course. Precisely: JSON is text (always double-quoted keys); a dict is a live object. You cross between them with json.dumps() / json.loads(). In your own Python, single or double quotes both work; a JSON document requires double quotes.

Topic 3
THE BLUEPRINT EVERY LIBRARY IS MADE OF

Classes & OOP

Why learn OOP if AI writes the code? Because you must read what it produces — and every library, and the agent you'll build, is “just classes.” Visualised live in Python Tutor.

You wouldn't create a hundred accounts as bank_account_1 = 123, bank_account_2 = 234… Instead you define one blueprint (the class) and stamp out as many objects as you like.

◆Definition

OOP = Object-Oriented Programming. A class is a blueprint; an object is a concrete instance stamped from it, carrying its own data (attributes) and behaviour (methods).

class BankAccount attrs: owner, balance, history methods: deposit() withdraw(), show_history() the blueprint acc1 · Mayankbalance 1000 acc2 · Krishbalance 500 acc3 · …its own state
One class → any number of objects. Each follows the same structure, each keeps its own data.
reconstructed from the session · python
class BankAccount:
    def __init__(self, owner: str, balance: float = 0):
        self.owner = owner          # set up THIS object's state
        self.balance = balance      # default 0 unless passed
        self.history = []

    def deposit(self, amount):
        self.balance += amount
        self.history.append(f"Deposited {amount}")

    def withdraw(self, amount):
        if amount > self.balance:        # guardrail: can't overdraw
            print("Insufficient funds"); return
        self.balance -= amount
        self.history.append(f"Withdrew {amount}")

    def show_history(self):
        for entry in self.history:
            print(entry)

acc1 = BankAccount("Mayank", 1000)
acc1.deposit(200)     # balance → 1200
acc1.withdraw(50)     # balance → 1150
__init__
Constructor

Runs once when the object is created; initialises attributes. Without a class you'd have no clean way to set up and carry this state.

SELF
self

The current object — lets each method read/update that specific account's data.

ATTR vs METHOD
Data vs behaviour

balance is an attribute; deposit() is a method. Access with a dot: acc1.balance.

WHY
Why it pays

“You get paid when you can go inside and learn why something is failing” — reading a framework's class Agent is this exact skill.

Student questionIs self the same as Java's this?
Instructor clarificationNot exactly — the instructor declined to over-simplify and pointed to a dedicated explainer. Treat self as the current object a method operates on; learn it properly because it appears everywhere in class-based code (including agents).
⬦Practical tip

Step through object creation in Python Tutor: you literally watch execution enter __init__, set owner/balance, then return a finished object. Seeing the pointer move makes “an object is a filled-in copy of the blueprint” click faster than prose.

Topic 4
LET PYTHON WRITE THE BORING CODE

Data classes

A @dataclass writes the boilerplate — __init__, __repr__, __eq__ — straight from type-hinted attributes. The Java-crowd shorthand: “it's like Lombok.”

◆Definition

Boilerplate = repetitive setup code you'd otherwise write every time (like boiling water before cooking). Developers are lazy about already-solved problems, so they build tools to skip it. For a data-holding class, that tool is @dataclass.

reconstructed from the session · python
from dataclasses import dataclass, field

@dataclass
class Book:
    title: str
    author: str
    pages_read: int = 0
    tags: list = field(default_factory=list)   # see the bug below

    def read(self, pages):
        self.pages_read += pages

    def add_tag(self, tag):
        self.tags.append(tag)

my_book = Book("Atomic Habits", "James Clear")
# No __init__ written by hand — the decorator generated it.
●Core idea

@dataclass is itself a decorator (Topic 6). It inspects the type-hinted attributes and auto-generates the constructor + comparison methods — you declare what data a Book holds and Python writes the plumbing. It's mainly for avoiding __init__; when you need full behaviour and getters/setters, write a normal class.

The line that confuses everyone: field(default_factory=list)

Why not just tags = []? Because a bare [] default is created once and secretly shared by every Book. Add a tag to one, it appears on all.

tags = [] ✗ shared book A book B ONE list ["AI"] add "AI" to A → B also gets "AI" default_factory=list ✓ book A book B list ["AI"] list [ ] each new book → a fresh, separate list
Left: one shared list, edits leak between objects. Right: default_factory builds a new empty list per object.
▲Watch out — the AI trap

“This is exactly the bug you'll never catch when you vibe-code and don't know the concept.” AI may hand you tags = []; it runs, then two objects mysteriously share tags in production. field(default_factory=list) means “make a new empty list every time a Book is created.”

✦Instructor insight — why it matters for agents

Later, instead of Book you'll define things like agent state and messages with this exact pattern. LangGraph and many custom agent frameworks represent structured state as data classes (or similar models) precisely because they keep code clean and easy to maintain.

Topic 5
SO YOUR AGENT DOESN'T DIE ON THE FIRST BAD CALL

Error handling

When AI writes code, the first version usually throws errors. And when your code hits an API or an AI, the call will sometimes fail. The question posed: should your whole agent die because one call failed? No — so you wrap the risky bit.

Kind of errorExampleMeaning
ZeroDivisionError10 / 0Runtime error — the operation is impossible
Syntax errorprnt(xCode written/spelled wrong — won't run at all
Logic error“numbers just look wrong”Runs fine, gives the wrong answer
reconstructed from the session · python
def safe_divide(a, b):
    try:
        result = a / b                 # the risky line
    except ZeroDivisionError:
        print("Can't divide by zero — using 0 instead")
        result = 0
    finally:
        print("Done attempting the division")   # ALWAYS runs
    return result
>>> safe_divide(10,2) → “Done attempting…” · 5.0
>>> safe_divide(10,0) → “Can't divide…” · “Done attempting…” · 0
TRY
try

Attempt the line that might blow up — an API hit, reading a key, parsing input.

EXCEPT
except

Catch the failure and recover instead of crashing the program (or the agent).

FINALLY
finally

Always runs, error or not. Classic use: close a database connection, or reset a value after a timeout.

▸Rule — wrap every fragile boundary

Reading an API key, making an API call, and making an AI call all belong inside try/except. A function written this way “will never fail” — it always returns something sane, which keeps an agent alive when a provider hiccups.

⌗Extra context — vocabulary

Python uses try / except and you raise an error (Java uses try / catch and throw). If you don't know the specific error type, catch a broad Exception — common failures here are connection/timeout errors.

Topic 6
HOW AGENT TOOLS GET THEIR SUPERPOWERS

Decorators

The analogy ran all session: you hand me a gift; I wrap it; the contents don't change, but what you get back is more than the plain box.

◆Definition

A decorator is a function that takes another function and returns a new, enhanced function — without changing the original's code.

greet()plain function @announcethe decorator wrapper()"Starting…" → greet() → "Finished…" the gift the wrapping paper the wrapped gift you call instead
@announce takes greet, returns a wrapper that does extra work before/after calling the original. You now call the wrapper.
reconstructed from the session · python
import functools

def announce(func):                 # takes a function…
    @functools.wraps(func)          # keep func's real __name__, __doc__
    def wrapper(*args, **kwargs):
        print(f"Starting {func.__name__}...")
        result = func(*args, **kwargs)   # run the original
        print(f"Finished {func.__name__}.")
        return result
    return wrapper                    # …returns an enhanced function

@announce
def greet(name):
    return f"Hello {name}"

print(greet("ma'am"))
>>> Starting greet... | Finished greet. | Hello ma'am
*args / **kwargs
Pass anything through

*args = positional args; **kwargs = keyword args (a dict of key–value pairs). The wrapper forwards them so it works for any signature.

WHY WRAPPER
Reuse

The nested wrapper exists so the decorator can be applied to any function, again and again.

▲Watch out — where None came from

If your wrapped function has no return, printing its result shows None — “a function that returns nothing gives you None.” That's why wrapper ends with return result.

The use case that makes it click: timing

reconstructed · python
import time, functools

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

@timed
def slow_addition(a, b):
    time.sleep(1); return a + b
✦Instructor insight — why it matters for agents

This is the mechanism behind agent tools. When you later write @tool above a function, a decorator is attaching the metadata (name, description, signature via func.__name__) the agent needs to discover and call it. Stacking is allowed too — with @timed above @announce, the order given was “first timed, then announced” (topmost = outermost wrapper).

Topic 7
AN “AI CALL” IS JUST ANOTHER REQUEST

Calling an API — and the “fake AI”

The big de-mystification: talking to ChatGPT/Claude is the same shape as calling any web API. Send a request in the format the service wants; get JSON back. A fake AI was built in between so the leap from “API” to “AI” feels small.

A real API with requests

Using the free Frankfurter currency API (real ECB rates). The response comes back as JSON — which you already know is a dict.

reconstructed from the session · python
import requests

def convert_currency(amount, from_ccy, to_ccy):
    url = f"https://api.frankfurter.app/latest?from={from_ccy}&to={to_ccy}"
    try:                                # this can fail → wrap it
        response = requests.get(url)
        data = response.json()          # APIs return JSON → a dict
        return amount * data["rates"][to_ccy]
    except Exception as e:
        print("API call failed:", e); return None
●Core idea

“All your APIs normally return you a JSON.” So the single most reusable skill for working with any service — including AI — is being fluent with dictionaries. Read the API's docs to learn the exact request shape (headers, body, JSON vs XML) it expects.

The “fake AI”: playing the role of ChatGPT

Rather than jump straight to a paid AI call, the instructor faked one — a function that plays the role of AI, taking the same model, max_tokens, messages inputs and returning a message.

your coderole: user AI (or fake AI)role: assistant request → messages [{role, content}] ← response (JSON: role + content)
Every AI turn carries a role (user vs assistant) and content. Input is JSON, output is JSON. A fake AI obeys the same contract.
reconstructed · python
def fake_ai(model, max_tokens, messages):
    user_message = messages[-1]["content"].lower()
    if "currency" in user_message or "convert" in user_message:
        reply = "I'd use a currency-conversion tool instead of guessing."
    elif "weather" in user_message:
        reply = "I'd use the weather tool."
    else:
        reply = "Ask me about currency or weather."
    return {"role": "assistant", "content": reply}   # a dict/JSON

for q in ["Convert $50 to Euro"]:
    response = fake_ai("gpt-x", 100, [{"role": "user", "content": q}])
    print(response["content"])
✦Instructor insight — the mindset to keep

“I don't care what API is present — real or fake. I give it the request the way it wants, I get the response.” The real client.chat.completions.create(model=…, messages=[…]) call you'll use later has this exact role/content shape. The next class swaps the fake AI for a real one.

⌗Extra context

The from __future__ import annotations line the instructor pasted is a best-practice detail (defers evaluating type hints) — treated as boilerplate you can look up, not something to master now.

Topic 8 · the main event
TYPE SAFETY FOR A LANGUAGE THAT HAS NONE

Pydantic

Python is dynamically typed — it will happily let a name be a number and an age be a string. Fine for a script, dangerous when data flows into a database, an API, or an agent.

◆Definition

Dynamically typed: a variable's type can change at runtime and Python doesn't enforce type hints. Pydantic is the library that lets you define the shape of your data (via BaseModel) and enforce it.

The problem: type hints don't enforce anything

reconstructed from the session · python
def create_user(name: str, age: int):
    return {"name": name, "age": age}

create_user("Mayank", "fifteen")   # runs fine — Python does NOT enforce the hint!

You could hand-write isinstance(...) checks — but you'd repeat them in create_user, update_user, delete_user… That doesn't scale.

The fix: a BaseModel

reconstructed from the session · python
from pydantic import BaseModel

class User(BaseModel):     # inherit BaseModel — no __init__ needed
    name: str
    age: int

u1 = User(name="Mayank", age=25)     # ✓ correct types
u2 = User(name="Mayank", age="25")   # ✓ Pydantic COERCES "25" → 25
u3 = User(name="Mayank", age=-5)     # ✓ -5 IS a valid int (see note!)
u4 = User(name="Rahul", age=[1,2,3])  # ✗ ValidationError: not an int
raw inputname, age User(BaseModel)validate + coerce ✓ User objectname="Mayank", age=25 ✗ ValidationError"input should be a valid integer"
Data enters the model; Pydantic either produces a clean, typed object or raises a ValidationError before bad data can spread.
▲Watch out — type validation ≠ data validation (yet)
  • Coercion: age="25" works because Pydantic type-casts the string to 25 (its default lax mode). A list can't become an int → error.
  • age=-5 passes because −5 is a valid integer. Rejecting “age must be > 0” is data validation (rules on values), which needs field constraints/validators — next class. So far you've enforced type validation only.
▸The three lines to remember

1. Pydantic creates rigid data models for inputs and outputs, making code more error-safe and agents/software better.
2. It enforces type (today) and data validation (next class).
3. It's what FastAPI is built on — when you expose your agents via API servers, Pydantic is already there. Everyone downstream assumes you know it.

Student questionCan Pydantic filter/extract just the fields I want out of heavy LLM text?
Instructor answerYes — define the structure, then extract into it. That's the seed of structured output from an LLM, and why Pydantic sits at the heart of agent I/O.
9 — QUESTIONS THAT MATTERED

Curated Q&A

From a long live Q&A, kept only where the answer teaches something reusable. Reveal each answer when you want it.

FrameworksIf I can build agents in plain Python, why use LangChain / LangGraph / CrewAI?▶

They're far more mature and take heavy lifting off you — like SMS vs WhatsApp. But understand the fundamentals first (it's just text in, text out) so a framework isn't a black box. Frameworks also tend to use more tokens — a real trade-off.

FrameworksNode.js or LangGraph — which path?▶

Category error: Node.js is a runtime/language; LangGraph is a framework. LangChain/LangGraph support TypeScript too, so you can use them from Node. Language doesn't matter much; the concepts do.

ProductionShould I use agents everywhere? What about token cost?▶

The instructor avoids agents where simple orchestration or a direct API call will do — agents burn more tokens and you lose control over what runs inside. Ask: does this genuinely need an agent? “Simple designs scale.”

ProductionOpen-source models or frontier (OpenAI/Claude)?▶

For client work, only frontier models shipped so far — they're battle-tested, and the infra cost of hosting/testing open models often outweighs the API cost. Clients rarely fund a $200 machine to trial many open models before a POC. Inside an org with its own hardware and strict data rules, the calculus can flip.

ProductionEmbedding-based RAG or non-embedding?▶

Depends on data + domain knowledge, decided after experimentation — not a fixed rule. Example: two projects, same problem; one used embeddings (large docs), the other a structured non-embedding approach (small warranty docs with a strong findable structure). Domain knowledge drives the system-design call.

CareerJob posts say “5+ years AI experience” — but AI is newer than that?▶

AI engineering = AI + software development. Those postings want strong SWE experience plus the ability to integrate AI — not literally N years of LLM work. For most product roles you're a consumer of models; transformer internals matter mostly at senior levels and for interviews (like DSA). Know ~60–70% of a JD comfortably.

ProductionCopilot gave me and a colleague different answers to the same prompt. Why?▶

Two reasons: (1) AI output is fuzzy / non-deterministic; (2) tools silently keep memory/context (workspace history, agent.md/claude.md instruction files) that differs between a long session and a fresh checkout. Fix with more explicit instructions, not just new sessions.

ConceptsWhy Pydantic instead of my own if checks?▶

Scaling. You have many functions (create/update/delete_user). Hand-rolled checks get copied into all of them and changing a rule means editing every copy. Define the model once; validation rides along.

10 — INSTRUCTOR & PRODUCTION NOTES

Reusable advice from the room

  • Simple designs scale. Reach for a plain API call + orchestration before an agent — more control, fewer tokens, still scales.
  • Read the code AI writes. “When it breaks, you're just sitting with AI.” The paid skill is going inside to see why it failed — which is why classes, decorators and types are non-negotiable.
  • AI is ~15–20% of a project. The other 80% is ordinary software and data work. The same problem can need embeddings or not, depending on data + domain knowledge.
  • Frontier models are usually the pragmatic client choice — battle-tested; self-hosting open models often costs more in infra/testing than it saves.
  • Better instructions > new sessions. AI is fuzzy and tools carry hidden context. Improve prompt specificity rather than blaming randomness.
  • uv is mature (~1.5 years); using it and a clean venv is what you say in an interview to show you can accelerate real development.
  • Interview lens. Transformer/DSA depth is asked to prove you can handle hard problems, even if daily work is mostly integration.
11 — WATCH-OUTS

Corrections worth pinning up

▲ WATCH OUT
Type hints don't enforce types

def f(age: int) still accepts "fifteen" — Python is dynamically typed. Enforcement needs Pydantic.

▲ WATCH OUT
tags = [] as a default

Creates one shared list across all instances. Use field(default_factory=list).

▲ WATCH OUT
obj["key"] vs obj.get("key")

Missing key: [] raises KeyError; .get() returns None.

▲ WATCH OUT
Pydantic and negative age

-5 passes — it's a valid int. Value rules (age > 0) are data validation, taught next class.

▲ WATCH OUT
Pydantic and string age

"25" is coerced to 25. Only truly incompatible types (a list where an int is needed) error out.

▲ WATCH OUT
Decorators don't change the function

They wrap it; you call a new wrapper that adds behaviour around an untouched original.

▲ WATCH OUT
None in output

If the wrapped function has no return, printing its result shows None. Add return result.

▲ WATCH OUT
Not every Windows terminal works

CMD/PowerShell don't recognise sh. Use Git Bash.

12 — GENERATED PRACTICE

Small exercises from this session

Learning exercises derived from what was taught (not transcript content). Run each in a notebook or with uv run; reveal a hint only after attempting.

EX 1 · RECALL · dict
Safe reader

Given {"city":"Tokyo","temp":21.5}, print the humidity without crashing, then loop and print every key/value.

Hint / check▶

.get("humidity") → None; then for k, v in d.items(): print(k, v).

EX 2 · APPLY · dataclass
Playlist

Make a @dataclass Playlist(name: str, songs) with songs defaulting to empty. Create two, add a song to one, confirm the other stays empty.

Hint / check▶

songs: list = field(default_factory=list). If you used = [], both would share the song — that's the bug to feel.

EX 3 · APPLY · errors
Bulletproof int

Write parse_age(x) returning int(x), or 0 if invalid, and always print “done”.

Hint / check▶

try: return int(x) / except ValueError: return 0 / finally: print("done").

EX 4 · REASON · decorator
Log-and-time

Write @timed on a function that sleeps 1s. Confirm it prints elapsed time and the real return value.

Hint / check▶

Remember return result inside wrapper, and @functools.wraps(func) to keep the name.

EX 5 · APPLY · api
Currency call

Hit Frankfurter to convert 50 USD→EUR. Print the raw .json(), then pull the rate out of the dict.

Hint / check▶

requests.get(url).json() → data["rates"]["EUR"], inside try/except.

EX 6 · REASON · pydantic
Break the model

Define User(BaseModel) with name: str, age: int. Predict which pass before running: age="30", age=-1, age=[1,2].

Hint / check▶

"30" → coerced to 30 (pass). -1 → valid int (pass). [1,2] → ValidationError.

13 — ACTIVE RECALL

Check yourself

Answer in your head (or explain aloud), then reveal.

1. Why can't you install uv from PowerShell the way the docs show?▶

The install script uses sh, which CMD/PowerShell don't recognise. Run it in Git Bash.

2. Explain dict.get("x") vs dict["x"] for a missing key.▶

.get() → None (no crash); [] → KeyError.

3. What breaks if a dataclass uses tags = [] as a default, and why?▶

All instances share one list — a tag added to one appears on all. The default is created once. Fix: field(default_factory=list).

4. Which of try/except/finally always runs, and give a real use.▶

finally — error or not. Classic use: closing a DB connection.

5. Define a decorator in one sentence, and say why the inner wrapper exists.▶

A function that takes a function and returns a new, enhanced one without changing the original. The nested wrapper exists so the decorator can be reused on any function.

6. What format does a web API return, and what Python type does it become?▶

JSON — which maps to a Python dict. An AI call has the same request/response shape.

7. Does User(name="Sam", age="25") fail for age: int? What about age=[1,2,3]?▶

"25" is coerced to 25 (passes). [1,2,3] raises a ValidationError.

8. Is enforcing “age must be positive” type or data validation?▶

Data validation (a rule on the value) — not covered yet; today was type validation.

9. Reconstruct from memory: what are the five Python pieces an agent is made of?▶

A class holding state · functions (some decorated) · API/AI calls returning JSON · error handling · Pydantic-validated inputs/outputs.

14 — QUICK REVISION

The whole session, compact

Session in 60 seconds

Get a Linux-style terminal (Git Bash) and uv so setups are reproducible. Refresh the primitives an agent is secretly built from: lists/dicts/JSON (APIs return JSON = dicts; use .get()), classes (blueprint + state), @dataclass (skip boilerplate; beware shared [] defaults), try/except/finally (wrap fragile calls), decorators (function → enhanced function; power agent tools), and API/AI calls (same request/response shape). Finish with Pydantic: dynamic typing is unsafe, so BaseModel enforces types (and, next class, data rules).

Core ideas

  • An agent = class + state + functions + API calls + error handling + Pydantic validation. Nothing exotic.
  • APIs (and AI) return JSON → fluency with dicts is the reusable skill.
  • Type hints don't enforce; Pydantic does (coerces where it can, errors where it can't).
  • Decorators wrap behaviour around an unchanged function — the mechanism behind agent tools.

Code patterns

reusable snippets
data.get("k")                         # safe dict read → None if missing
tags: list = field(default_factory=list)  # dataclass mutable default (never = [])
try: risky()
except Exception as e: recover(e)
finally: cleanup()                    # always runs (close DB)
@functools.wraps(func)                # inside a decorator's wrapper
data = requests.get(url).json()      # → dict
class User(BaseModel): name: str; age: int   # typed, validated

Instructor takeaways

✦

Learn the basics so frameworks aren't a black box — then LangChain is easy.

✦

You're paid to know why code fails, not to run it. Go inside the library.

✦

Simple designs scale; avoid agents unless the problem truly needs one (tokens + control).

✦

AI is ~15–20% of a project; the rest is ordinary software and data work.

Recall prompts

Cover the notes and answer: Why Git Bash over PowerShell? · What does field(default_factory=list) prevent? · One sentence: what is a decorator? · Why does User(age="25") succeed but User(age=[1,2,3]) fail? · What's the difference between type and data validation?

Study artifact reconstructed from the Day-2 session recording (Agentic 3.0 GenAI bootcamp, 28 Jun 2026, instructor Mayank Aggarwal). The transcript is the primary teaching source; code marked ◐ reconstructed is a clean, runnable version of what was narrated on screen (the instructor typed while talking), not a verbatim capture. Editor clarifications and extra context are labelled and kept distinct from what was taught. Where a claim was a deliberate teaching simplification (e.g. “dict = JSON”, “Python has no array”), the nuance is noted without overwriting the intent.