AI EngineeringZero to ProductionHome·About·What’s new·Contact
Appendix · Python for AI Agents · Part 1

Python Basics

Python is the language every lab and the capstone are written in. This four-part appendix takes you from zero to the advanced Python this course actually uses — and every concept links to the exact place in the course where you'll see it. Start here if Python is new or rusty; skim if you're fluent.

⏱️ ~60 min🎯 Absolute beginner OK🔗 linked to course code

Learning objectives

  • Run Python and understand variables, the core types, and strings.
  • Use f-strings — the way every prompt and message in this course is built.
  • Write conditionals and loops — the shape of the agent loop and eval loops.
  • Define functions with parameters, defaults, and return values.
How this appendix worksEach concept has a 🔗 Used in the course-style box pointing at the real file/lab where it appears. Learn the concept here, then click through to see it doing real work. You don't need to memorize — you need to recognize it when you meet it.

Why Python for AI agents? essential

Here's a fear worth naming out loud, because almost everyone arriving at an AI course carries it: "I don't really know how to code — am I even allowed to be here?" If that's you, breathe. You are exactly who this page is for. You will not need to become a "programmer" to build the agent in this course; you'll need to read and write a small, specific slice of one friendly language, and this appendix hands you that slice piece by piece.

So why Python and not something else? Think of programming languages like spoken languages: you could learn Latin, but if everyone you want to talk to speaks Spanish, you learn Spanish. In the AI world, Python is Spanish — it's the language the tools, tutorials, sample code, and colleagues all already speak. When you hit a problem at 11pm, the answer you find online will almost always be written in Python. That network effect alone is worth more than any single clever language feature.

And Python earns that popularity by being unusually readable. A line like if confidence < 0.6: send_to_human() reads almost like plain English, which matters more than beginners expect: you will spend far more time reading code — yours from last week, someone else's, the course labs — than writing it from a blank page. The common beginner mistake here is treating Python like a maths exam, panic-memorizing syntax. Don't. You're learning to recognize patterns, the same way you recognize a stop sign without ever reciting its exact dimensions.

Every major AI SDK (including the Anthropic SDK this course uses) is Python-first. Python's readable syntax, huge ecosystem, and libraries like Pydantic (data validation) make it the default language for LLM apps, RAG, and agents. You don't need to be a Python expert to start — you need the slice of Python this course actually uses, which is exactly what these four parts cover.

PartCoversUnlocks
P1 (here)Types, strings, control flow, functionsReading & writing any lab
P2Lists, dicts, comprehensions, JSON, files, regexMessages, chunks, fixtures, log parsing
P3Modules, imports, OOP, dataclasses, enums, type hintsThe agent's structure & tool registry
P4Pydantic, exceptions, generators, decorators, async, env/secretsStructured output, streaming, safety, keys

1 · Running Python basic

The first hurdle in programming is rarely the code — it's the strange, empty feeling of "okay… where do I even type this?" Plenty of people watch a tutorial, see code on the screen, and never once run a line themselves because nobody showed them the boring mechanical step of how. So let's kill that hurdle first, because it's the smallest one dressed up as the scariest.

Think of Python as a very literal assistant who reads a to-do list from top to bottom and does exactly what each line says, in order, never skipping ahead. A .py file is that to-do list — a plain text file of instructions. When you "run Python," you're handing the assistant the list and saying "go." That's the whole idea; there's no magic ceremony.

You'll meet two everyday ways to run it. The first is running a whole file (python first_call.py) — you use this once your program is more than a couple of lines, which is nearly always. The second is a quick one-liner (python -c "...") for when you just want to test a tiny idea without opening a file at all — the programmer's equivalent of scribbling on a sticky note. A common early confusion is expecting a "run" button somewhere; in this course you type a command, press Enter, and the assistant reports back. Comments — anything after a # — are notes to humans; the assistant reads right past them. Two ways you'll run code in this course:

two front doors — both hand code to the same interpreter a saved file python first_call.py a quick one-liner python -c "print('hi')" Python interpreter reads top → bottom output printed A file or a one-liner — same engine. Both routes hand your instructions to the one Python interpreter, which runs them top to bottom and prints the result; there is no "run button", just a command you type.
🗺️ How to read this diagram

Read it left to right: the two entry boxes on the left are the two commands you type, and both arrows lead into the single interpreter that does the work.

  • The top-left box is running a saved .py file — what you do once your program is more than a line or two.
  • The bottom-left box is the python -c "…" one-liner — source code handed directly on the command line, for testing a tiny idea.
  • Both arrows converge on the white interpreter box, which reads the instructions top to bottom, and the green box is the payoff — whatever your code printed.

In short: file or one-liner, the code lands in the same interpreter — you type a command and press Enter, there is no run button to hunt for.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Try it
terminal# 1. run a file
python first_call.py

# 2. a quick one-liner
python -c "print('hello agents')"

A .py file is just a list of instructions run top to bottom. Comments start with # and are ignored.

🔗 Used in the courseEvery lab is a .py file you run this way — starting with Chapter 1, Lab 1.3 (first_call.py).

2 · Variables & the core types basic

Imagine you're cooking from a recipe and it keeps saying "the sauce," "the sauce," "the sauce." You don't re-describe the whole pot of tomatoes-onions-and-garlic every time — you gave it a short name, "the sauce," and now everyone in the kitchen knows what you mean. A variable is exactly that: a short, reusable name you stick on a value so you can refer to it later without repeating yourself.

The best mental picture is a labelled box. max_tokens = 1024 means "put the value 1024 in a box, and write max_tokens on the label." Later, whenever you write max_tokens, Python opens that box and hands you what's inside. Change your mind? Put something else in the box — the label stays, the contents update. That single = is doing something surprisingly specific: it's not "equals" in the maths sense, it's an arrow, "take the value on the right and store it under the name on the left."

Now, the part that quietly matters: every value has a type — a category that tells Python what kind of thing it is and what you're allowed to do with it. Text is one type, whole numbers another, decimals another. Python is friendly here: it figures out the type automatically from what you put in the box (this is called dynamic typing), so you never have to declare "this box holds a number." The classic beginner trap is confusing the number 7 with the text "7" — they look identical on screen but behave completely differently (7 + 7 is 14; "7" + "7" is "77"). Watch for the quotes; quotes mean "this is text." The five types below are the entire vocabulary you need for now, and you'll recognize every one of them in the course's real agent code. A variable is a name for a value. Python figures out the type automatically.

name = value — the name is the label, the value goes in the box max_tokens = 1024 stores label: max_tokens 1024 type? int (auto) one label per box · Python reads the value to pick the type str "opus-4-8" int 1024 float 0.7 bool True None no value A variable is a labelled box. The = takes the value on the right and stores it under the name on the left; Python inspects that value to auto-pick its type — one of the five shown along the bottom.
🗺️ How to read this diagram

Read the top row left to right — an assignment becomes a labelled box — then scan the bottom row for the five types a box can hold.

  • The top-left box is the line you write, max_tokens = 1024; the solid arrow shows it stored into the green box, which carries the label (max_tokens) and the contents (1024).
  • The dashed arrow to the far-right box is the payoff of dynamic typing: Python reads the value and picks the type for you (here int) — you never declare it.
  • The bottom row is the whole vocabulary you need for now — str, int, float, bool, None — each a box holding an example value.

In short: label on the outside, value on the inside, and Python figures out the type from the value itself.

Try it
pythonmodel = "claude-opus-4-8"     # str  (text)
max_tokens = 1024             # int  (whole number)
temperature = 0.7            # float (decimal)
streaming = True              # bool (True / False)
result = None                 # None (absence of a value)

print(type(model))            # <class 'str'>
TypeExampleWhere it shows up
str"claude-opus-4-8"model names, prompts, tool names
int1024max_tokens, restart counts, step caps
float0.72confidence scores, cosine similarity
boolTrueneeds_human, allowed in the safety gate
NoneNone"no runbook matched", optional fields
🔗 Used in the coursemodel, max_tokens in Ch 1; confidence: float and needs_human: bool in the Ch 2 Ticket schema; allowed: bool in the capstone's audit record.

3 · Strings & f-strings basic → essential

Picture a fill-in-the-blanks form letter: "Dear ______, your order of ______ has shipped." The template never changes; you just slot in a different name and item for each customer. Before this feature existed, programmers built messages by gluing fragments together by hand — "Pod " + pod + " in " + ns + " is crash-looping" — a fiddly, error-prone dance of plus-signs and stray spaces that everyone got wrong at least once. The f-string is Python's fill-in-the-blanks form letter, and it's genuinely one of the nicest things in the language.

First, the word itself: a string is just Python's name for text — any run of characters wrapped in quotes, like "staging" or "You are a careful SRE". (The name is old-timey computing for "a string of characters threaded together." It sticks around; don't overthink it.) A string is what you send to a model as a prompt, what comes back as an answer, and what you write to a log — so text handling isn't a side topic in AI work, it's most of the job.

The f-string turns text from a fixed thing into a template. You put an f right before the opening quote, then anywhere you want a value to appear, you write {a_variable} in curly braces. Python swaps each blank for the current value at the moment the line runs. So f"Pod {pod} in {ns} is crash-looping" becomes "Pod checkout-api in staging is crash-looping" — readable, no plus-signs, no misplaced spaces. The number-one beginner mistake is forgetting the f: without it, "Pod {pod}" prints the literal braces and the word pod, not the value — if you ever see stray {curly braces} in your output, a missing f is almost always why. Strings are text. The single most important tool here is the f-string — it drops variables into text. Every prompt, every message, every log line in this course is built with f-strings.

the f plus {braces} turn fixed text into a fill-in-the-blanks template pod "checkout-api" ns "staging" template f"Pod {pod} in {ns} is crash-looping" → {pod} → {ns} run final text Pod checkout-api in staging … An f-string is a template, not fixed text. Each {name} is a blank; when the line runs, Python drops the current value of that variable into the blank — which is why prompts and log lines are built this way instead of gluing strings with +.
🗺️ How to read this diagram

Read it left to right: the variables on the left flow into the template in the middle, which produces the finished text on the right.

  • The two blue boxes on the left are your variables — pod and ns — each holding a piece of text.
  • The arrows show each variable's value flowing into the matching {pod} / {ns} blank in the white template box. The f before the quote is what tells Python to treat the braces as blanks rather than literal characters.
  • The green box on the right is the payoff: the moment the line runs, every blank is replaced by its value, giving one clean sentence with no stray braces and no plus-signs.

In short: variables in, blanks filled, finished text out — that substitution is the entire idea of an f-string.

Try it
Setup to run this snippet
class _Any:
    '''stands in for any undefined demo value; supports call/attr/index/
    iteration and basic arithmetic (as 0.7) so demo snippets run.'''
    def __call__(self, *a, **k): return _Any()
    def __getattr__(self, k): return _Any()
    def __getitem__(self, k): return _Any()
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __contains__(self, o): return True
    def __enter__(self, *a): return _Any()
    def __exit__(self, *a): return False
    def __float__(self): return 0.7
    def __int__(self): return 1
    def __lt__(self, o): return True
    def __gt__(self, o): return False
    def __le__(self, o): return True
    def __ge__(self, o): return False
    def __add__(self, o): return o
    def __radd__(self, o): return o
    def __bool__(self): return True
    def __repr__(self): return 'demo'
    def __str__(self): return 'demo'
logs = _Any()
pythonpod = "checkout-api"
ns = "staging"

# f-string: prefix the quote with f, put variables in {curly braces}
msg = f"Pod {pod} in {ns} is crash-looping"
print(msg)          # Pod checkout-api in staging is crash-looping

# multi-line strings use triple quotes — used for system prompts
system = """You are a careful SRE.
Diagnose using read-only tools first."""

# handy string methods
"HELLO".lower()               # 'hello'
"  hi  ".strip()             # 'hi'  (trim whitespace)
"a,b,c".split(",")          # ['a', 'b', 'c']
"\n".join(["a", "b"])         # 'a\nb'  (glue a list into text)
"error" in logs             # True/False — substring check
🔗 Used everywheref-strings build the RAG context block in Ch 3 Lab 3.4 (f"[{i}] (source: {c['source']})..."), the incident messages in the capstone, and every log line in Ch 6 observability. .lower()/.split()/in power the runbook retriever in Lab 8c.

4 · Booleans & comparisons basic

Every decision your program will ever make comes down to a yes/no question. Should we trust this answer? Is this action dangerous? Did the search find anything? A computer can't act on "maybe" or "sort of" — it needs a clean yes or no. That yes/no value has a name in Python: a boolean (named after George Boole, a 19th-century logician, but you can just think "true/false switch"). There are exactly two of them: True and False.

The everyday analogy is a light switch: it's either on or off, never "kind of on." Booleans are the switches your program flips to decide what to do next. But where do these True/False values come from? Mostly from comparisons — you ask Python a question about two values and it hands you back a boolean. restarts > 5 is literally the question "is the number in restarts bigger than 5?" and Python answers True or False.

Here's the single most infamous beginner trap in all of Python, and it's worth burning into memory now: = and == are not the same thing. One equals sign assigns ("put this value in the box," from the last section); two equals signs ask ("are these two things equal?"). Writing if restarts = 0 when you meant if restarts == 0 is a rite of passage — Python will even stop and complain, which is its way of catching you. You can also chain questions together with the plain-English words and (both must be true), or (at least one is true), and not (flip it). Those three little words are how the course's safety gate reasons — "this action is irreversible and we're not just observing" is a real decision an agent makes before it's allowed to act.

a comparison asks a question → Python hands back one boolean restarts > 5 asks True restarts is 7, so 7 > 5 holds the trap: one = stores, two == ask x = 0 assign — put 0 in the box x == 0 ask — is x equal to 0? → True/False Comparisons produce booleans; = and == are not the same. Asking restarts > 5 yields True/False — while a single = stores a value and a double == asks whether two values match.
🗺️ How to read this diagram

Read the top row left to right — a question becomes a boolean — then read the bottom row as the one distinction beginners trip on.

  • The top-left box is a comparison, restarts > 5; the arrow shows Python answering it with a single green boolean — here True, because restarts holds 7.
  • The bottom-left amber box is = doing assignment — it stores a value in the box, it asks nothing.
  • The bottom-right blue box is == doing a question — "are these two equal?" — which is what produces a boolean. Mixing them up (if x = 0) is the classic first-week bug.

In short: every yes/no in your program is a comparison returning a boolean — and one = stores while two == ask.

Try it
pythonrestarts = 7
restarts > 5            # True
restarts == 0           # False  (== is "equals", = is "assign")
restarts != 0           # True   (!= is "not equal")

risk = "irreversible"
risk == "irreversible" and rung == "observe"   # combine with and / or / not
confidence < 0.6                                # route low-confidence to a human
🔗 Used in the courseconfidence < 0.6 routing in Ch 2 classifier; if resp.stop_reason == "refusal" in Ch 1; the entire safety gate in Lab 8c is comparisons (if risk == RiskClass.IRREVERSIBLE).

5 · Control flow: if / for / while essential

Up to now your program has been a straight road — the assistant reads the to-do list top to bottom, one line after another. But real work isn't a straight road; it has forks ("if it's raining, take an umbrella") and repetition ("for every dish in the sink, wash it"). Control flow is the name for the handful of tools that let your program branch and repeat instead of marching in a single line — and honestly, this is the section where code stops feeling like a list and starts feeling alive.

There are three tools and each maps to a plain-English idea. if is a fork in the road: "if this is true, do this; otherwise do that." for is "do this once for each item in a pile" — for each pod, for each message, for each test case. while is "keep doing this until something changes" — and that last one is quietly the heart of the whole course: an AI agent is a while loop that keeps thinking and acting until the task is done or a safety cap stops it.

Now the one piece of Python that trips up newcomers more than any other: indentation is not decoration — it's grammar. Most languages wrap a block of code in curly braces { } to show "these lines belong together." Python throws the braces away and uses whitespace instead: the lines that belong to an if or a for are the ones indented underneath it (four spaces, by universal convention). A colon : ends the header line and announces "a block is coming." Think of it like an outline or a bulleted list — the sub-points are indented under their heading, and everyone reading knows they're subordinate. The classic beginner error, IndentationError, is simply Python saying "your indentation doesn't line up, so I can't tell what belongs to what" — annoying at first, but it's the reason Python code is so consistently readable. Python uses indentation (4 spaces) to group code — no braces. The colon : starts a block.

three shapes — a fork, a walk over items, and a loop that repeats until it stops if / elif / else a fork — one branch runs confidence > 0.8? yes no trust it else… only ONE branch ever runs for item in list once per item pod 1 pod 2 pod 3 the body runs for each row while cond: repeat until it stops step < MAX_STEPS? do work · step += 1 loop back stop: cap hit / break Three ways to leave the straight road. if picks one branch, for runs the body once per item, and while loops back and repeats until its condition flips or a cap stops it — that last shape is literally the agent loop.
🗺️ How to read this diagram

Read the three panels left to right; each one is a separate control-flow tool, and inside each panel the arrows show the path the program takes.

  • The left panel (if) is a fork: the question at the top splits into a yes path (solid, to the green box) and a no path (dashed, to the else box). Only one of them ever runs — the point of a branch.
  • The middle panel (for) is a walk over a list: each row is one item, and the arrow off each row means "run the indented body once for this item," top to bottom.
  • The right panel (while) is the loop that matters most: while the condition holds it does work and the dashed arrow loops back to re-check; when the cap is hit or break fires, the solid arrow exits to the red stop box. The loop-back is what makes it repeat.

In short: if chooses, for walks a pile, while repeats until it's told to stop — and an agent is that while loop.

Try it
Setup to run this snippet
MAX_STEPS = 5           # demo constant (a hard cap)
confidence = 0.7          # demo threshold value
done = False       # demo flag
pods = [
    {"id": "x1", "text": "demo one", "name": "api-7f9c", "status": "Running"},
    {"id": "x2", "text": "demo two", "name": "worker-2d", "status": "CrashLoopBackOff"},
]
python# if / elif / else — decisions
if confidence > 0.8:
    print("trust it")
elif confidence > 0.5:
    print("double-check")
else:
    print("send to human")

# for — do something for each item
for pod in pods:
    if pod["status"] == "CrashLoopBackOff":
        print(pod["name"])

# while — repeat until a condition changes (the agent loop!)
step = 0
while step < MAX_STEPS:      # a hard cap so it always ends
    step += 1
    if done:
        break                 # exit the loop early
▶ How this works

This block shows Python's three ways to make decisions and repeat work. Read the indentation carefully — in Python, the indented lines belong to the if, for or while above them. That indentation is not decoration; it is how Python knows what's inside the block.

  1. if / elif / else — Python checks confidence > 0.8 first. If it's true, it prints "trust it" and skips the rest. If not, it tries elif (else-if). If none match, else runs. Only one branch ever runs.
  2. for … in pods — this repeats the indented code once for each item in the list pods. Each time, pod holds the current item. Inside, we check one field and print the name only for crash-looping pods.
  3. while step < MAX_STEPS — this repeats as long as the condition stays true. We add 1 to step each pass so it eventually reaches the cap and stops. break exits the loop immediately if done becomes true.

What the output means: Nothing prints from the while loop here (it's just structure), but the for loop would print worker-2d — the one pod whose status is CrashLoopBackOff.

Try this: Change confidence to 0.4 and predict which branch prints before you run it. Then add a third pod to the list and watch the for loop pick up the crash-looping ones.

Indentation is syntax in PythonCode inside an if/for/def must be indented consistently (4 spaces). Mixing tabs and spaces, or wrong indentation, is the #1 beginner error — IndentationError.
🔗 This IS the agent loopThe for ... in range(MAX_STEPS) loop is the literal heart of Ch 4 and the capstone engine. for case in golden_set drives the Ch 5 evals. for block in resp.content reads the model's response everywhere.

6 · Functions essential

You've probably felt this without a name for it: you copy-paste the same six lines for the third time and a little voice says "this is dumb." It's right. The moment you find yourself repeating a chunk of code, you've discovered why functions exist. A function is a named, reusable recipe — you write the steps once, give them a name, and from then on you just call the name instead of copying the steps. Change the recipe in one place and every use of it updates. That's not a nicety; it's the difference between code you can maintain and code that rots.

A microwave is the cleanest analogy. You press "popcorn" — you don't need to know the magnetron timings, the power cycling, any of the internals. You hand it a bag (the input) and it hands you back popcorn (the output). A function is the same deal: you define the messy steps once, hidden inside, and callers just push the button. The values you hand in are called parameters (the ingredients), and the value handed back comes via the word return (the finished dish). You define a function with def, but — and this catches everyone once — defining it doesn't run it. Writing the recipe card isn't cooking. You have to call it later, by writing its name with parentheses: get_logs("checkout-api").

One more idea earns its keep here: default parameters. Some ingredients have a sensible standard value, so you make them optional. If namespace almost always means "staging", you set that as the default and callers only mention it when they want something else. This is exactly how the Anthropic SDK works — max_tokens, temperature and friends have defaults, and you override just the ones you care about. A common early stumble is forgetting the parentheses when calling (writing get_logs instead of get_logs()), which hands you the recipe card itself rather than running it. A function is a named, reusable block. def defines it; return sends a value back. Parameters can have defaults.

inputs in → hidden steps run once → one result out (via return) arguments "checkout-api" namespace="prod" lines=50 in def get_logs(…) the recipe — steps hidden inside return result "last 50 log lines for…" def writes the recipe; calling it get_logs(…) with () actually cooks A function is a microwave. You hand it inputs (parameters), its hidden steps run, and return hands back one result — and defining it with def is not running it; you must call it with ().
🗺️ How to read this diagram

Read it left to right, the way a value flows through: arguments enter on the left, the function body transforms them in the middle, and one result leaves on the right.

  • The left blue box holds the arguments you pass in — the required pod plus the optional namespace and lines you chose to override.
  • The middle white box is the function body — the reusable steps you wrote once under def, hidden from the caller who just "presses the button".
  • The right green box is what return hands back — a single result. The bottom note is the trap: writing def only defines the recipe; you must call it with parentheses to make anything happen.

In short: parameters in, hidden steps run, one value out — and it stays inert until you call it with ().

Try it
pythondef get_logs(pod, namespace="staging", lines=20):
    # pod is required; namespace & lines have defaults
    return f"last {lines} log lines for {namespace}/{pod}"

get_logs("checkout-api")                    # uses the defaults
get_logs("checkout-api", "prod")            # positional args
get_logs("checkout-api", lines=50)         # keyword arg (clearer)
▶ How this works

A function is a reusable recipe: you give it inputs (called parameters) and it hands back a result with return. Defining a function does not run it — you call it later by writing its name with parentheses.

  1. def get_logs(pod, namespace="staging", lines=20): defines the recipe. pod is required (no default). namespace and lines have defaults, so you can leave them out and get "staging" and 20.
  2. The return line builds a string with an f-string and sends it back to whoever called the function.
  3. The three calls show the choices: use all defaults; pass values by position; or name them (lines=50) for clarity — called a keyword argument.

What the output means: Each call produces a sentence, e.g. get_logs("checkout-api") returns "last 20 log lines for staging/checkout-api".

Try this: Call get_logs("db", "prod", 5) and read the result. Then try get_logs() with no arguments — you'll get a TypeError because pod is required. That error message is Python telling you a required input is missing.

Keyword arguments = readable codeCalling complete(messages, model="claude-opus-4-8", effort="high") is far clearer than remembering argument order. This course uses keyword args heavily — and so do both LLM SDKs: the Claude/Anthropic SDK (client.messages.create(model=..., max_tokens=...)) and the OpenAI SDK (client.responses.create(model=..., max_output_tokens=...)).
🔗 Used in the courseThe client wrapper complete(...) in Ch 1; get_logs(pod, namespace="staging") is a real function in the capstone mock cluster; answer(question, k=4) in Ch 3. The **block.input "unpacking" that calls tools (Ch 4) builds directly on keyword args — you'll meet it in P2.

7 · Numbers & math — token cost, scores, similarity basic → essential

"But I'm here for AI, not to be a human calculator." Fair — and the good news is you already know most of this from a school calculator; Python just uses slightly different buttons. The reason numbers matter here isn't abstract maths, it's money and quality: every call to a model costs real dollars based on how much text goes in and out, and every evaluation of your agent boils down to a score you average. If you can't do the arithmetic, you can't answer "how much is this costing me?" or "is this version actually better?" — the two questions that decide whether an AI project survives.

Python gives you two kinds of number and the distinction is worth ten seconds. An int is a whole number (1024, 7, -3) — think counting things: tokens, retries, steps. A float is a number with a decimal point (0.7, 3.5) — think measuring things: a confidence score, a similarity, a price. Most operators are exactly what you'd expect (+ - *), but three deserve a spotlight because beginners meet them and blink. Regular division / always gives a decimal (7 / 2 is 3.5, even when it divides evenly). Floor division // throws away the remainder and keeps the whole part (7 // 2 is 3) — useful for "how many complete groups fit." And the strange-looking modulo % gives you just the remainder (7 % 2 is 1); the classic trick is n % 2 == 0 to test if a number is even, or "do something every Nth time."

The single most common beginner surprise, though, is that decimals aren't always exact: type 0.1 + 0.2 and Python answers 0.30000000000000004, not 0.3. That's not a bug in Python — it's how computers store fractions in binary, and every language does it. The practical lesson is simply: don't test floats for exact equality; round for display (you'll see round(...) and f-string formatting next), and compare with a small tolerance when it matters. You'll do real arithmetic in this course: computing API cost, averaging eval scores, normalizing similarity. Python has two number types (int, float) and the usual operators, plus a few you'll actually use.

two number types, three ways to divide int — whole numbers count: tokens, retries, steps1024, 7, -3 float — has a decimal point measure: score, similarity, price0.7, 3.5 the three that surprise beginners — all on 7 and 2 7 / 2 → 3.5 true division (float) 7 // 2 → 3 floor: drop remainder 7 % 2 → 1 modulo: just remainder Whole vs decimal, and three divisions. int counts, float measures; on the same 7 and 2, / keeps the decimal (3.5), // keeps the whole part (3), and % keeps the leftover (1).
🗺️ How to read this diagram

Read the top row as the two number types, then the bottom row as the three division operators — all shown acting on the same 7 ÷ 2 so you can compare them directly.

  • The top-left box is int — whole counting numbers; the top-right box is float — numbers with a decimal point, for measuring.
  • The three bottom boxes apply the confusing operators to identical inputs: / always gives a decimal, // throws the remainder away and keeps the whole part, and the green % box keeps only the remainder.
  • That green % is the payoff — n % 2 == 0 is the standard "is it even?" / "every Nth item" test you'll reuse constantly.

In short: int counts and float measures; / keeps the decimal, // keeps the whole part, % keeps the leftover.

Try it
pythonin_tokens, out_tokens = 1240, 380

# operators
7 / 2          # 3.5   true division (always float)
7 // 2         # 3     floor division (drops the remainder)
7 % 2          # 1     modulo (remainder) — "every Nth item"
2 ** 10        # 1024  power

# the real API-cost formula (Opus: $5/1M in, $25/1M out)
cost = (in_tokens * 5 + out_tokens * 25) / 1_000_000   # underscores = readable numbers

# helpers you'll reach for
round(cost, 6)                 # round to 6 dp
sum([0.8, 0.9, 1.0]) / 3   # 0.9  — an average (eval pass rate)
min(30, retries * 2)        # cap a backoff delay
max(0.0, score)             # clamp a score to be non-negative
abs(a - b)                  # distance between two numbers
🔗 Used in the courseThe cost formula is Ch 1 Exercise 1.1. Averaging pass/fail gives the Ch 5 accuracy score (passed / len(golden)). min() caps retry backoff in Ch 6. RRF in Ch 3 is 1/(k+rank) — division you now understand.

8 · Formatting numbers & text in f-strings essential

Show a raw number to a human and watch them squint. A cost that prints as 0.012345600000001 is technically correct and completely unreadable; nobody wants twelve decimal places of a penny. A report where the dollar figures don't line up in a column is a report people misread. From the last section you know decimals can trail long ugly tails — and here's where you tidy that up. Formatting is the difference between output a machine produced and output a person can actually use, and in a field where you're constantly reporting costs and scores to humans, it's not cosmetic — it's the job.

The lovely part: you don't need a separate tool. The same f-strings you already met grow a small "styling" language. Inside the curly braces, you add a colon and then a short instruction: {cost:.4f} means "show cost as a float with 4 decimal places." Read the colon as "…formatted like this." A few of these mini-instructions cover almost everything you'll want: .2f for a fixed number of decimals, .0% to display a fraction as a percentage (0.85 → 85%), , to drop in thousands separators (1240 → 1,240), and a width number like {name:20} to pad text into a fixed-width column so things line up.

The mental model to hold onto: formatting changes only how a value is displayed, never the value itself. Rounding a cost to 4 decimals for a log line doesn't lose the real number sitting in the variable — you're choosing what the human sees, not editing your data. The beginner mistake is reaching for clumsy manual tricks (chopping strings, multiplying and dividing to round) when a two-character format spec does it cleanly. Learn the handful below and your output instantly looks like it came from someone who's done this before. f-strings do more than insert values — a : inside the braces controls formatting. This is how every clean log line and report in the course is produced.

the : after a value = "…displayed like this" (the stored value never changes) stored value cost = 0.0123456 format spec {cost:.4f} display 0.0123 a few specs cover almost everything — same idea, different mini-instruction {score:.0%} 0.85 → 85% percentage {1240:,} 1240 → 1,240 thousands sep {name:20} pad to width 20 columns line up Formatting changes the display, not the value. The raw 0.0123456 stays put in the variable; the spec after the : decides what the human sees — .4f, .0%, , and a width are the handful you'll actually use.
🗺️ How to read this diagram

Read the top row left to right — value, then spec, then display — then treat the bottom row as a small menu of the specs you'll reach for.

  • The top-left box is the real number sitting in the variable; it is untouched by formatting. The middle box is the format spec — the colon plus a short instruction inside the braces.
  • The green top-right box is what actually prints — here 0.0123, trimmed to four decimals for a human, while the stored value keeps all its digits.
  • The three bottom boxes are the everyday specs: .0% shows a fraction as a percentage, , adds thousands separators, and a width number pads text so columns line up in a report.

In short: the spec after the : decides only what the reader sees — the number in the box never changes.

Try it
pythoncost = 0.0123456
score = 0.847
name = "checkout-api"

f"${cost:.4f}"          # '$0.0123'   — 4 decimal places
f"{score:.0%}"          # '85%'       — as a percentage
f"{name:20}"            # 'checkout-api        ' — pad to width 20 (left-align)
f"{name:>20}"           # right-align in width 20
f"{1240:,}"             # '1,240'     — thousands separator
f"{'PASS' if ok else 'FAIL'}"   # inline conditional inside an f-string
🔗 Used in the coursef"{fb:20} -> {s.label} ({s.confidence:.2f})" aligns the Ch 2 classifier output. f"[{'PASS' if ok else 'FAIL'}]" is the exact Ch 5 eval print. Percentage and dollar formatting appear throughout the cost/observability code.

9 · Truthiness & the None checks that prevent crashes essential

Here is the crash that will happen to you, probably this week: your code asks a search for results, the search finds nothing, hands back an empty list, and the next line confidently tries to use "the first result" — that doesn't exist — and the whole program falls over with a red error. Every experienced programmer has shipped this bug. The fix is a small habit called truthiness, and learning it now will save you hours of confusion later.

The idea: Python lets you put a value directly inside an if, not just a comparison. When you write if results:, Python asks "is there anything meaningful here?" The rule for the answer is beautifully simple — empty and zero-like things are "falsy," everything else is "truthy." An empty list [], an empty string "", the number 0, and the special value None all count as false; a list with items, a non-empty string, any non-zero number all count as true. Think of a mailbox: if mailbox: reads as "if there's any mail," and an empty box quietly means "no." This is what lets you write the tidy guard if not hits: — "if we got no hits" — instead of clunky length checks.

The special character in this story is None — Python's way of saying "deliberately nothing here," the absence of a value (an empty runbook reference, an optional field nobody filled in). Two things trip people up. First, None is falsy, so if value: treats a missing value as false — handy, but watch the edge: the number 0 is also falsy, so if score: is false when the score is legitimately 0.0, which may not be what you meant. Second, the correct way to test for None is if x is None, not if x == None — use is for this one specific check (it asks "is this the exact None object?"). Master this section and you've inoculated yourself against the single most common beginner crash: assuming a value is there when it isn't. Python treats empty things as falsy. Understanding this makes your guards concise — and prevents the most common agent crash: assuming a value exists.

every value is either falsy (empty/zero-like) or truthy — if x: asks which falsy — treated as False None False 0 0.0 "" [] {} "empty or nothing here" truthy — treated as True "staging" 42 0.6 ["a"] {"k": 1} "there's something here" if not hits: hits is [] → falsy → guard fires return "no information" early exit — no crash on empty data Empty is falsy; everything else is truthy. Because an empty list is falsy, if not hits: catches the "search found nothing" case and returns early — the one-line guard that stops the most common beginner crash.
🗺️ How to read this diagram

Read the top row first — the two buckets every value falls into — then follow the arrow down into the guard that uses them.

  • The red box (top-left) lists the falsy values: None, False, zero, and the empty containers. Put any of these in an if and it counts as false.
  • The green box (top-right) is everything else — non-empty strings, non-zero numbers, filled lists and dicts — all truthy.
  • The bottom row is the payoff: when hits is an empty list it lands in the falsy bucket, so if not hits: is true, the arrow flows right, and the function returns early instead of reaching for a result that isn't there.

In short: knowing which bucket a value falls into lets one short guard replace a crash with a clean early exit.

Try it

Illustrative fragment — defines demo values / files are needed before this runs standalone.

python# falsy: None, False, 0, 0.0, "" (empty str), [] (empty list), {} (empty dict)
# everything else is truthy

if not hits:                 # True when hits is [] — "no results"
    return "I don't have that information."

if results:                  # True only when the list is non-empty
    process(results)

# the None-safe pattern used all over the course:
text = next((b.text for b in resp.content if b.type == "text"), "")
#     ^ generator                                              ^ default if none found

# check for None explicitly with 'is' (not ==)
if runbook_ref is None:
    runbook_ref = "(none)"

# the "or default" shortcut
name = pod.get("name") or "unknown"   # falls back if None/empty
▶ How this works

"Truthy" and "falsy" mean: when you put a value directly in an if, Python decides true/false for you. Empty things are falsy (None, 0, "", [], {}); everything else is truthy. This lets your safety checks stay short.

  1. if not hits: reads as "if there are no hits". When hits is an empty list [] (falsy), not [] is true, so we return the 'no information' message instead of crashing on missing data.
  2. if results: runs the body only when the list actually has items — a clean way to say "if we found something".
  3. The next((... for b in resp.content if ...), "") line grabs the first matching piece of the model's reply, and the "" at the end is a safety default: if nothing matches, you get an empty string instead of an error.
  4. is None is the correct way to check for 'no value'. pod.get("name") or "unknown" falls back to "unknown" when the name is missing or empty.

Try this: Set hits = [] then hits = ["a"] and see which one triggers the 'no information' return. This exact pattern is how the course's RAG code avoids crashing when a search finds nothing.

is None vs == NoneUse is None to check for None — it's the idiomatic, correct way (is checks identity). Reserve == for comparing values. And remember 0 and "" are falsy — if score: is False when score is 0.0, which may not be what you want; use if score is not None: when zero is a valid value.
🔗 Used in the courseif not hits: is the "no runbook / no results" guard in the Ch 3 RAG answer and the Lab 8c retriever. The next(..., "") default is how every response-reading line avoids a crash when there's no text block (e.g. on a refusal).

10 · The if __name__ == "__main__" guard essential

You'll see this cryptic line — if __name__ == "__main__": — at the bottom of nearly every real Python file, and it looks like a secret handshake. Beginners either copy it around like a magic charm or nervously delete it. Neither is right, and the truth is refreshingly practical, so let's demystify it.

Here's the situation it solves. A Python file can play two different roles. Sometimes you run it directly — python rag_store.py — because you want it to do something right now. Other times you import it from another file — from rag_store import VectorStore — because you just want to borrow a function or class it defines, not run the whole thing. The problem: if the file has a demo or a self-test at the bottom, that demo would fire every time anyone imported it — like a cookbook that started cooking every recipe the moment you opened it to copy one down. Nobody wants that.

The guard is Python's answer. When Python runs a file, it secretly sets a hidden variable called __name__: if the file is the one you launched directly, __name__ equals the text "__main__"; if the file was imported by someone else, __name__ is instead the file's own name. So the line if __name__ == "__main__": literally reads "only do the following if I'm the file being run, not the file being imported." Everything indented under it — your quick demo, a self-test, a main() call — runs on python thisfile.py and stays politely silent on import. You don't need to fully grok the machinery today; recognize the shape, know it means "run this part only when this file is the star of the show," and put your own demos there. Code under this guard runs only when the file is executed directly — not when it's imported. It's how a file can be both a runnable script and an importable module. Nearly every module in the capstone uses it.

one file, two roles — __name__ tells the file which one it is rag_store.py defs + a demo block python rag_store.py import rag_store run directly __name__ == "__main__" imported by another file __name__ == "rag_store" guard is true → demo RUNS guard is false → demo SKIPPED Same file, two roles — __name__ decides. Run the file directly and __name__ is "__main__" so the guarded demo fires; import it and __name__ is the module's name, so only its defs load and the demo stays silent.
🗺️ How to read this diagram

Start at the file box on top, then follow the two arrows down — the left path is "run directly," the right path is "imported."

  • The top box is the file itself: some functions and classes, plus a demo block at the bottom guarded by if __name__ == "__main__":.
  • The left path is python rag_store.py: Python sets __name__ to "__main__", the guard is true, and the arrow leads to the green box — the demo runs.
  • The right path is import rag_store: Python sets __name__ to the module's own name, the guard is false (dashed arrow), and the amber box shows the demo is skipped — only the defs load, so importing never triggers the self-test.

In short: the guard reads "run this part only when I'm the file being launched," letting one file be both a script and an importable module.

Try it

Illustrative fragment — defines demo values / files are needed before this runs standalone.

rag_store.pydef embed(texts):
    ...                       # importable by other files

class VectorStore:
    ...

if __name__ == "__main__":      # only runs on: python rag_store.py
    # a quick self-test / demo
    store = VectorStore(); store.add(load_corpus())
    for chunk, score in store.search("reset password", k=3):
        print(f"[{score:.3f}] {chunk['source']}")

Run directly → the demo runs. from rag_store import VectorStore in another file → only the function/class are imported, the demo does not run.

🔗 Used in the courseEvery capstone module with a "run me to test" block: rag_chunk.py, rag_store.py, rag.py, and the evals' if __name__ == "__main__": main() (Ch 5, Lab 8d).

Worked example · a tiny cost tracker putting it together

Individual ingredients don't teach you to cook — the first time you make a whole dish is when it clicks. You've now met every piece of P1 on its own: variables, types, f-strings, comparisons, loops, functions, arithmetic, formatting, the main guard. This section is where they stop being isolated facts and become a program that answers a real question you'll ask constantly: "how much did these API calls cost me?"

Don't be intimidated by seeing it all at once — that "wall of code" feeling fades the moment you realize it's just the same small ideas standing next to each other. Read it the way you'd read a paragraph, not a wall: a dictionary holding prices (a lookup table, name → value), a function doing the arithmetic once so it isn't repeated, a loop walking through several calls, and formatted printing so the output is human-readable. The habit worth stealing here is how to read unfamiliar code: skim the whole thing for shape first, then walk it top to bottom asking "what does this one line do?" — the exact skill you'll use on every lab in this course, and on real code for the rest of your career. Everything in P1 combined into one small, course-relevant program — types, f-strings, a loop, a function, math, formatting, and the main guard.

how the pieces of P1 wire together into one program PRICES dict model → (in, out) calls list (model, in, out) rows call_cost(…) the arithmetic, once loop · total += cost walk every call, sum as you go printed report aligned lines + TOTAL The whole program is a small pipeline. A lookup dict and a list of calls feed one call_cost function; a loop runs each call through it and sums a running total; f-string formatting turns that into an aligned, human-readable report.
🗺️ How to read this diagram

Follow the arrows from the two data sources on the left, through the function and loop in the middle, to the printed report on the right.

  • The two left boxes are the data: the PRICES dict (a lookup table, model → rates) and the calls list (the rows to price). Both flow into the function.
  • The white box is call_cost — the arithmetic written once so it isn't repeated; the amber box below is the loop that runs each call through it and adds each cost onto a running total.
  • The green box is the payoff: formatted f-string output — one aligned line per call plus the summed TOTAL. Each box maps to a section you already met (dict, function, loop, formatting).

In short: data in (dict + list) → priced once by a function → summed in a loop → printed as a tidy report; that is every earlier P1 idea standing side by side.

Worked example
cost_tracker.py# in/out token prices per 1M tokens, by model
PRICES = {
    "claude-opus-4-8":  (5, 25),
    "claude-haiku-4-5": (1, 5),
}

def call_cost(model, in_tokens, out_tokens):
    in_price, out_price = PRICES[model]           # tuple unpack
    return (in_tokens * in_price + out_tokens * out_price) / 1_000_000

# pretend these came back from resp.usage on several calls
calls = [
    ("claude-opus-4-8", 1240, 380),
    ("claude-haiku-4-5", 600, 40),
    ("claude-opus-4-8", 8000, 1200),
]

def main():
    total = 0.0
    for model, tin, tout in calls:
        c = call_cost(model, tin, tout)
        total += c
        print(f"{model:18} in={tin:6,} out={tout:5,}  ${c:.5f}")
    print(f"{'TOTAL':18} {'':17}  ${total:.5f}")

if __name__ == "__main__":
    main()
claude-opus-4-8    in= 1,240 out=  380  $0.01570
claude-haiku-4-5   in=   600 out=   40  $0.00080
claude-opus-4-8    in= 8,000 out=1,200  $0.07000
TOTAL                                   $0.08650
▶ How this works

This is everything in P1 working together in one small program: a dictionary of prices, a function that does the math, a loop over several calls, and formatted output. It answers a real question — "how much did these API calls cost?"

  1. PRICES is a dictionary: it maps each model name to a pair (input_price, output_price) per million tokens. Look-up by name later.
  2. call_cost(model, in_tokens, out_tokens) reads that model's two prices (in_price, out_price = PRICES[model] unpacks the pair), then applies the cost formula and divides by 1,000,000 because prices are per million tokens.
  3. calls is a list of three (model, in, out) tuples — pretend results from three real API calls.
  4. main() loops over those calls, adds each cost to a running total, and prints an aligned line per call. The if __name__ == "__main__": main() guard means main() runs only when you execute this file directly.

What the output means: Each line shows the model, its token counts (with comma separators), and that call's cost to 5 decimals; the last line is the summed TOTAL ($0.08650).

Try this: Add a fourth call to the calls list — say a big Haiku call — and re-run. The total updates automatically because the loop and the running total do the work.

Exercises advanced

Exercise P1.1 — f-string diagnosis line

Context: f-strings are the everyday way to build readable diagnostic lines, interpolating variables straight into text — the format every log and status message uses.

Your task: Given pod = "web-5abc" and restarts = 3, print web-5abc has restarted 3 times using an f-string.

Requirements:

  • Use an f-string
  • Interpolate both variables
  • Match the exact output wording

💡 Hint: One print(f"...") with {pod} and {restarts} embedded in the sentence.

Solution
Setup to run this snippet
class _Any:
    '''stands in for any undefined demo value; supports call/attr/index/
    iteration and basic arithmetic (as 0.7) so demo snippets run.'''
    def __call__(self, *a, **k): return _Any()
    def __getattr__(self, k): return _Any()
    def __getitem__(self, k): return _Any()
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __contains__(self, o): return True
    def __enter__(self, *a): return _Any()
    def __exit__(self, *a): return False
    def __float__(self): return 0.7
    def __int__(self): return 1
    def __lt__(self, o): return True
    def __gt__(self, o): return False
    def __le__(self, o): return True
    def __ge__(self, o): return False
    def __add__(self, o): return o
    def __radd__(self, o): return o
    def __bool__(self): return True
    def __repr__(self): return 'demo'
    def __str__(self): return 'demo'
pod = _Any()
restarts = _Any()
print(f"{pod} has restarted {restarts} times")

Exercise P1.2 — the risk decision

Context: An agent decides what to do from a risk label, and the canonical shape is an if/elif/else mapping each risk level to an action — a preview of the Lab 8c gate.

Your task: Write an if/elif/else that prints allow, ask, or block for a risk of read_only, reversible, or irreversible.

Requirements:

  • Three branches mapping each risk to its action
  • read_only → allow
  • reversible → ask
  • irreversible → block

💡 Hint: A straight if/elif/else on the risk string; this is the exact shape the agent's approval gate uses.

Solution
Setup to run this snippet
class _Any:
    '''stands in for any undefined demo value; supports call/attr/index/
    iteration and basic arithmetic (as 0.7) so demo snippets run.'''
    def __call__(self, *a, **k): return _Any()
    def __getattr__(self, k): return _Any()
    def __getitem__(self, k): return _Any()
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __contains__(self, o): return True
    def __enter__(self, *a): return _Any()
    def __exit__(self, *a): return False
    def __float__(self): return 0.7
    def __int__(self): return 1
    def __lt__(self, o): return True
    def __gt__(self, o): return False
    def __le__(self, o): return True
    def __ge__(self, o): return False
    def __add__(self, o): return o
    def __radd__(self, o): return o
    def __bool__(self): return True
    def __repr__(self): return 'demo'
    def __str__(self): return 'demo'
risk = _Any()
if risk == "read_only":
    print("allow")
elif risk == "reversible":
    print("ask")
else:
    print("block")

Exercise P1.3 — a capped loop

Context: Every agent loop is a bounded loop that stops early on success. Capping iterations and breaking on a condition is the shape you'll reuse constantly.

Your task: Write a for loop that runs at most 5 times and stops (break) as soon as a variable found is True.

Requirements:

  • Loop bounded to at most 5 iterations
  • Break as soon as found is True
  • Uses a for loop and break

💡 Hint: Iterate over range(5) and break the moment found becomes True — the anatomy of every agent loop.

🎯 Interview practice interview

The interview questions this topic gets asked — worked, with code. For the full pattern catalog see A9 · Big Tech AI-engineering patterns.

FizzBuzz — the classic warm-up

Print 1..n, but multiples of 3 → "Fizz", of 5 → "Buzz", of both → "FizzBuzz". Tests clean control flow and operator precedence.

pythondef fizzbuzz(n):
    for i in range(1, n + 1):
        out = ""
        if i % 3 == 0: out += "Fizz"
        if i % 5 == 0: out += "Buzz"
        print(out or i)   # empty string is falsy -> print the number
▶ How this works

FizzBuzz is the classic warm-up: print 1 to n, but replace multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of both with "FizzBuzz". The trick is to build the answer up instead of writing every case separately.

  1. for i in range(1, n + 1): counts i from 1 up to n. range(1, n+1) stops before n+1, so the last value is n.
  2. i % 3 == 0 means "the remainder when dividing i by 3 is zero" — i.e. i is a multiple of 3. If so, we glue "Fizz" onto out. Same idea for 5 → "Buzz".
  3. Because both ifs can run, a number divisible by both 3 and 5 collects "FizzBuzz" automatically — no special case needed.
  4. print(out or i) uses truthiness: if out is still an empty string (falsy — the number was a multiple of neither), it prints the number i instead.

Try this: Run fizzbuzz(15) and check line 15 says FizzBuzz. This one line — print(out or i) — is what interviewers look for; it shows you understood truthiness.

Reverse an integer without str()

Pop digits with %10 and build the reverse with *10. Tests number sense, not string tricks.

pythondef reverse_int(x):
    sign = -1 if x < 0 else 1
    x, out = abs(x), 0
    while x:
        out = out * 10 + x % 10   # append last digit
        x //= 10
    return sign * out
▶ How this works

This reverses the digits of an integer (1234 → 4321) using only math — no converting to a string. It's a favorite interview question because it tests whether you understand how digits work with % (remainder) and // (whole-number division).

  1. sign = -1 if x < 0 else 1 remembers whether the number was negative, then x = abs(x) works with the positive value so the math is simple.
  2. x % 10 gives the last digit (1234 % 10 = 4). x // 10 chops that digit off (1234 // 10 = 123).
  3. out = out * 10 + x % 10 shifts what we've built so far one place left and tacks the new digit on the end — so 4, then 43, then 432, then 4321.
  4. while x: keeps going until x becomes 0 (0 is falsy). Finally we re-apply the sign.

Try this: Trace reverse_int(120) by hand: digits 0, 2, 1 build 021 = 21. Leading zeros vanish naturally because they're just 0 * 10 + 0.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Token-cost of one callBeginner

Context: Every LLM call has a price, and the arithmetic is the same everywhere: input and output tokens each priced per million. This is the one calculation an agent repeats on every request.

Your task: Write call_cost(in_tokens, out_tokens, in_price, out_price) and print the cost of 1240 in / 380 out at $5/$25 per 1M tokens, rounded to 5 decimals.

Requirements:

  • Cost = (in×in_price + out×out_price) / 1_000_000
  • Prices are per million tokens
  • Return a float
  • Format the result to 5 decimal places with an f-string

💡 Hint: It's one arithmetic expression and an f-string; divide by 1_000_000 because prices are quoted per million tokens.

Show solution

Straight arithmetic and an f-string, exactly as in the worked example. Runnable:

def call_cost(in_tokens, out_tokens, in_price, out_price):
    return (in_tokens * in_price + out_tokens * out_price) / 1_000_000

c = call_cost(1240, 380, 5, 25)
print(f"${c:.5f}")   # $0.01570
Exercise 2 · Price several models from a dictIntermediate

Context: Real code prices many models, so per-model rates live in a dict and calls are processed in a loop. Aligned columns and a total make the output readable.

Your task: Store per-model (in_price, out_price) tuples in a dict, loop over a list of (model, in, out) calls printing each cost in aligned columns, and a total.

Requirements:

  • A prices dict keyed by model name
  • Tuple-unpack the rates on lookup
  • Loop over the calls accumulating a total
  • Print aligned columns using f-string field widths
  • Print a TOTAL line

💡 Hint: Dict lookup + tuple unpack + a running sum; f-string field widths ({x:6}) give the aligned columns.

Show solution

Dict lookup + tuple unpack + a loop + f-string field widths, as in the P1 worked example. Runnable:

PRICES = {
    "claude-opus-4-8":  (5, 25),
    "claude-haiku-4-5": (1, 5),
}

def call_cost(model, tin, tout):
    in_price, out_price = PRICES[model]
    return (tin * in_price + tout * out_price) / 1_000_000

calls = [("claude-opus-4-8", 1240, 380), ("claude-haiku-4-5", 600, 40)]
total = 0.0
for model, tin, tout in calls:
    c = call_cost(model, tin, tout)
    total += c
    print(f"{model:18} in={tin:6,} out={tout:5,} ${c:.5f}")
print(f"{'TOTAL':18} {'':17} ${total:.5f}")
Exercise 3 · Guard bad input with control flowAdvanced

Context: Production code guards its inputs. An unknown model should fail clearly and negative token counts are invalid, so the cost function validates before it computes.

Your task: Extend call_cost to validate: an unknown model returns a clear signal, negative token counts raise ValueError.

Requirements:

  • Negative token counts raise ValueError
  • An unknown model returns a sentinel (e.g. None) rather than crashing
  • A valid call still returns the correct cost
  • Uses if, in, and raise

💡 Hint: Check the token counts and model membership up front with if/raise; return the sentinel for the unknown model before doing any arithmetic.

Show solution

Uses if, in, and raise ValueError from sections 4-5. Runnable:

PRICES = {"claude-opus-4-8": (5, 25)}

def call_cost(model, tin, tout):
    if tin < 0 or tout < 0:
        raise ValueError("token counts must be non-negative")
    if model not in PRICES:
        return None                      # signal 'unknown model'
    in_price, out_price = PRICES[model]
    return (tin * in_price + tout * out_price) / 1_000_000

print(call_cost("claude-opus-4-8", 1000, 200))   # 0.01
print(call_cost("ghost-model", 10, 10))          # None
try:
    call_cost("claude-opus-4-8", -1, 0)
except ValueError as e:
    print("rejected:", e)
Exercise 4 · A reusable cost tracker with a running totalExpert

Context: A running cost tracker needs to hold state across calls. A closure captures that state without a class — still just functions — which is the plain-Python way to a reusable helper.

Your task: Wrap the tracker in a helper that returns a closure (or a dict of functions) recording each call and reporting the running total and per-model breakdown.

Requirements:

  • A factory returns record/report functions sharing captured state
  • record computes and accumulates each call's cost
  • A per-model breakdown is maintained
  • report returns the running total and the breakdown
  • Uses nonlocal (or a mutable) to update captured state

💡 Hint: A closure over a total and a by_model dict keeps state without a class; nonlocal lets record update the total.

Show solution

A closure captures mutable state without a class — still just functions from section 6. Runnable:

def make_tracker(prices):
    total = 0.0
    by_model = {}
    def record(model, tin, tout):
        nonlocal total
        in_p, out_p = prices[model]
        cost = (tin * in_p + tout * out_p) / 1_000_000
        total += cost
        by_model[model] = by_model.get(model, 0.0) + cost
        return cost
    def report():
        return {"total": round(total, 5), "by_model": by_model}
    return record, report

record, report = make_tracker({"opus": (5, 25), "haiku": (1, 5)})
record("opus", 1000, 200); record("haiku", 500, 50)
print(report())
Exercise 5 · Format a spend reportProfessional

Context: Reporting spend to a human means formatting: models sorted by cost, right-aligned dollar figures with thousands separators, and a total — all with f-strings alone.

Your task: Produce a monthly report string: models sorted by cost descending, each line a right-aligned cost with a $ and thousands separator, plus a TOTAL line.

Requirements:

  • Sort models by cost descending
  • Right-align the cost with a fixed width
  • Use a thousands separator and 2 decimals
  • Prefix each figure with $
  • End with a TOTAL line
  • Use only f-string formatting

💡 Hint: sorted(..., key=lambda kv: -kv[1]) for descending, and f-string specs like {cost:>10,.2f} for the aligned money column.

Show solution

Sorting + f-string alignment, the P1 formatting toolkit. Runnable:

spend = {"claude-opus-4-8": 12.4, "claude-haiku-4-5": 0.8, "claude-sonnet": 3.15}

def report(spend):
    lines = []
    for model, cost in sorted(spend.items(), key=lambda kv: -kv[1]):
        lines.append(f"{model:20} ${cost:>10,.2f}")
    total = sum(spend.values())
    lines.append(f"{'TOTAL':20} ${total:>10,.2f}")
    return "\n".join(lines)

print(report(spend))
Exercise 6 · Budget guard for an agent loopIndustry scenario

Context: An agent that makes many calls can run away on cost. The production pattern is a per-run USD budget checked before each call, stopping the loop the moment the next call would exceed it.

Your task: Before each call, check a per-run USD budget; if the next call's estimated cost would exceed it, stop and report how many calls ran and the spend. Simulate 100 calls.

Requirements:

  • Estimate each call's cost before making it
  • Stop before a call that would exceed the budget
  • Report the number of calls made and the total spend
  • Run over a simulated list of ~100 calls
  • Deterministic — no API needed

💡 Hint: Accumulate spend and break before the call that would push you over; report the loop index and the rounded spend at the stopping point.

Show solution

Combines everything in P1 into a realistic guard — the pattern a production agent uses to avoid runaway spend. Runnable (deterministic, no API):

def estimate(tin, tout, in_p=5, out_p=25):
    return (tin * in_p + tout * out_p) / 1_000_000

def run_until_budget(budget_usd, calls):
    spent = 0.0
    for i, (tin, tout) in enumerate(calls):
        cost = estimate(tin, tout)
        if spent + cost > budget_usd:
            print(f"stopping before call {i}: budget ${budget_usd} would be exceeded")
            break
        spent += cost
    return {"calls_made": i, "spent": round(spent, 5)}

calls = [(1200, 300)] * 100          # 100 identical calls, ~0.0135 each
print(run_until_budget(0.05, calls))  # stops after a few calls

✓ Checkpoint — you're ready for P2 when you can…

  • Run a .py file and a one-liner.
  • Name the five core types and give an example of each from the course.
  • Build a message with an f-string and use .lower()/.split()/in.
  • Write if/elif/else and a capped for/while loop.
  • Define a function with a default parameter and call it with a keyword arg.

Knowledge check check yourself

✓ Knowledge check

What does an f-string do, and why is it called the single most important string tool in this course?

Show answer
An f-string (prefix the quote with f and put variables in {curly braces}) drops variable values into text. Every prompt, message, and log line in the course is built with f-strings — e.g. f"Pod {pod} in {ns} is crash-looping".
✓ Knowledge check

How does Python group a block of code (e.g. the body of an if or a for loop), and what starts a block?

Show answer
Python uses indentation — 4 spaces — to group code, not braces, and a colon : starts the block. This indentation-based structure is the shape of the agent loop and the eval loops throughout the course.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in