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.
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.
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.
| Part | Covers | Unlocks |
|---|---|---|
| P1 (here) | Types, strings, control flow, functions | Reading & writing any lab |
| P2 | Lists, dicts, comprehensions, JSON, files, regex | Messages, chunks, fixtures, log parsing |
| P3 | Modules, imports, OOP, dataclasses, enums, type hints | The agent's structure & tool registry |
| P4 | Pydantic, exceptions, generators, decorators, async, env/secrets | Structured 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:
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
.pyfile — 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.
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.
.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.
= 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.
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.
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'>
| Type | Example | Where it shows up |
|---|---|---|
str | "claude-opus-4-8" | model names, prompts, tool names |
int | 1024 | max_tokens, restart counts, step caps |
float | 0.72 | confidence scores, cosine similarity |
bool | True | needs_human, allowed in the safety gate |
None | None | "no runbook matched", optional fields |
model, 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.
{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 +.
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 —
podandns— 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. Thefbefore 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.
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
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.
= 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.
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 — hereTrue, becauserestartsholds 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.
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
confidence < 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.
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.
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 orbreakfires, 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.
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
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.
- if / elif / else — Python checks
confidence > 0.8first. If it's true, it prints"trust it"and skips the rest. If not, it trieselif(else-if). If none match,elseruns. Only one branch ever runs. - for … in pods — this repeats the indented code once for each item in the list
pods. Each time,podholds the current item. Inside, we check one field and print the name only for crash-looping pods. - while step < MAX_STEPS — this repeats as long as the condition stays true. We add 1 to
stepeach pass so it eventually reaches the cap and stops.breakexits the loop immediately ifdonebecomes 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.
if/for/def must be indented consistently (4 spaces). Mixing tabs and spaces, or wrong indentation, is the #1 beginner error — IndentationError.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.
return hands back one result — and defining it with def is not running it; you must call it with ().
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
podplus the optionalnamespaceandlinesyou 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
returnhands back — a single result. The bottom note is the trap: writingdefonly 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 ().
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)
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.
def get_logs(pod, namespace="staging", lines=20):defines the recipe.podis required (no default).namespaceandlineshave defaults, so you can leave them out and get"staging"and20.- The
returnline builds a string with an f-string and sends it back to whoever called the function. - 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.
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=...)).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.
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).
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 isfloat— 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 == 0is 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.
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
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.
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.
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.
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
f"{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.
if not hits: catches the "search found nothing" case and returns early — the one-line guard that stops the most common beginner crash.
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 anifand 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
hitsis an empty list it lands in the falsy bucket, soif 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.
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
"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.
if not hits:reads as "if there are no hits". Whenhitsis an empty list[](falsy),not []is true, so we return the 'no information' message instead of crashing on missing data.if results:runs the body only when the list actually has items — a clean way to say "if we found something".- 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. is Noneis 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.if 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.
__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.
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.
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.
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.
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.
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
PRICESdict (a lookup table, model → rates) and thecallslist (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 runningtotal. - 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.
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
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?"
PRICESis a dictionary: it maps each model name to a pair(input_price, output_price)per million tokens. Look-up by name later.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.callsis a list of three(model, in, out)tuples — pretend results from three real API calls.main()loops over those calls, adds each cost to a runningtotal, and prints an aligned line per call. Theif __name__ == "__main__": main()guard meansmain()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
foundis True - Uses a
forloop andbreak
💡 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.
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
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.
for i in range(1, n + 1):countsifrom 1 up to n.range(1, n+1)stops before n+1, so the last value is n.i % 3 == 0means "the remainder when dividing i by 3 is zero" — i.e. i is a multiple of 3. If so, we glue"Fizz"ontoout. Same idea for 5 →"Buzz".- Because both
ifs can run, a number divisible by both 3 and 5 collects"FizzBuzz"automatically — no special case needed. print(out or i)uses truthiness: ifoutis still an empty string (falsy — the number was a multiple of neither), it prints the numberiinstead.
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.
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
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).
sign = -1 if x < 0 else 1remembers whether the number was negative, thenx = abs(x)works with the positive value so the math is simple.x % 10gives the last digit (1234 % 10 = 4).x // 10chops that digit off (1234 // 10 = 123).out = out * 10 + x % 10shifts 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.while x:keeps going untilxbecomes 0 (0 is falsy). Finally we re-apply thesign.
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.
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
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}")
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, andraise
💡 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)
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())
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))
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
.pyfile 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/elseand a cappedfor/whileloop. - Define a function with a default parameter and call it with a keyword arg.
Knowledge check check yourself
What does an f-string do, and why is it called the single most important string tool in this course?
Show answer
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".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
: starts the block. This indentation-based structure is the shape of the agent loop and the eval loops throughout the course.