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
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
Two ways you'll run code in this course:
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
A variable is a name for a value. Python figures out the type automatically.
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
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.
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
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
Python uses indentation (4 spaces) to group code — no braces. The colon : starts a block.
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
A function is a named, reusable block. def defines it; return sends a value back. Parameters can have defaults.
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 does the Anthropic SDK (client.messages.create(model=..., max_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
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.
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
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.
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
Python treats empty things as falsy. Understanding this makes your guards concise — and prevents the most common agent crash: assuming a value exists.
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
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.
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
Everything in P1 combined into one small, course-relevant program — types, f-strings, a loop, a function, math, formatting, and the main guard.
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.