AI EngineeringZero to ProductionHome·About·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

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

Two ways you'll run code in this course:

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

A variable is a name for a value. Python figures out the type automatically.

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

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.

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

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

Python uses indentation (4 spaces) to group code — no braces. The colon : starts a block.

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

A function is a named, reusable block. def defines it; return sends a value back. Parameters can have defaults.

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 does the Anthropic SDK (client.messages.create(model=..., max_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

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.

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

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.

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

Python treats empty things as falsy. Understanding this makes your guards concise — and prevents the most common agent crash: assuming a value exists.

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

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.

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

Everything in P1 combined into one small, course-relevant program — types, f-strings, a loop, a function, math, formatting, and the main guard.

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