01

The Shape of Python

Variables, strings, lists, tuples — the building blocks Claude uses in every script

Names & Values

When you ask Claude to store a username or count, this is exactly what it writes. Variables are the simplest idea in programming — and the most powerful.

Variables are Luggage Tags

Variables don't contain data — they point to data. Like a luggage tag: the tag isn't the suitcase, it's a label attached to one.

PYTHON
user_name = "Alice"
score = 42
is_active = True
last_login = None
PLAIN ENGLISH

Create a label "user_name" pointing to the text Alice

Label "score" pointing to the number 42

A yes/no flag — True means yes, this user is active

Intentionally empty — this user has never logged in. None is not zero, it means "no value yet"

Glossary: variable · snake_case · None

🐍

Python uses snake_case (underscores between words) for variable names. JavaScript uses camelCase. When you tell Claude to store a user name, it automatically writes user_name in Python.

Strings Are More Than Text

Strings Have Superpowers

A string is any text in Python. What makes it powerful is the dozens of methods built right in — verbs you can call on any text.

📥
Raw Input
✂️
.strip()
🔡
.lower()
📝
f-string
Click "Next Step" to begin
💡

The f in f-string stands for formatted. The {} curly braces are windows into your code — whatever is inside gets substituted with its value. Claude uses f-strings constantly.

Glossary: string · method · f-string

Lists vs Tuples

Lists vs Tuples — Change vs Permanence

Both hold sequences of items. The difference is mutability — whether the contents can change after creation.

⚠️

Common mistake: trying to modify a tuple. Python will throw a TypeError. This is a feature — tuples protect data that should never change after creation.

Glossary: list · tuple · mutable · immutable

Reading Lists

Indexing and Slicing

Once you have a list, you need to read specific items. Python gives you powerful tools for this — including counting from the back.

PYTHON
tasks = ["buy milk", "write report", "call Alice", "review PR"]

first = tasks[0]      # "buy milk"
last  = tasks[-1]     # "review PR"
two   = tasks[1:3]    # ["write report", "call Alice"]
rev   = tasks[::-1]   # reversed list
PLAIN ENGLISH

Index 0 is the first item. Python counts from 0, not 1

Negative index: count from the back. -1 is always the last item

Slice from index 1 up to (not including) index 3

Slice with step -1: walk backward through the whole list — reverses it

🔢

Python counts from 0. The first item is at position 0. After a few days this feels natural — and you start finding 1-based indexing strange.

Glossary: index · slice · negative index

List Comprehensions

Comprehensions — Lists Built in One Line

Comprehensions are Python's compact way to build a new list from an existing one — with optional filtering and transformation. Claude uses them constantly.

📋
Source List
🔍
Filter
Transform
Result
Click "Next Step" to begin
PYTHON
numbers = [1, 2, 3, 4, 5, 6, 7, 8]

# Long way
result = []
for n in numbers:
    if n % 2 == 0:
        result.append(n * n)

# Comprehension — same result
result = [n * n for n in numbers if n % 2 == 0]
PLAIN ENGLISH

Long way: make empty list, loop, check if even, square it, add it

 

Short way: [expression for item in source if condition]

Both produce [4, 16, 36, 64]

Claude writes the short form — now you can read it

💡

When you ask Claude to filter a list and transform each item, it almost always writes a comprehension. [x * 2 for x in items if x > 0] means: go through items, keep the positive ones, double each one.

Check Your Understanding

You have a list of filenames. Which expression gives only the .py files?

Your code has if cart_items: to check if the cart has items. A colleague says use len(cart_items) > 0. Who is right?

Match each Python type to its property:

list
tuple
str

Mutable (changeable after creation)

Drop here

Immutable, ordered, used for fixed records like coordinates

Drop here

Immutable sequence of characters

Drop here
02

Making Decisions

Conditionals, truthiness, dictionaries — the logic layer behind every Claude-generated program

Truthiness — Python's Yes and No

Python evaluates more values than just True and False. Any non-empty, non-zero value is truthy. Any empty or zero value is falsy. This is how Claude checks if a list has items, if a name was provided, if a user is logged in.

Truthy Values

Non-zero numbers, non-empty strings, non-empty lists — Python treats these as True in conditions

Falsy Values

0, 0.0, empty string, empty list, empty dict, None, False — Python treats these as False

==

Value Equality

== checks if two values are equal: 'admin' == 'admin' is True. Use for comparing data.

is

Identity Check

is checks if two variables point to the same object. Only use is for None: if user is None:

PYTHON
users = []
name = ""
count = 0
user = None

if users:              # empty list → False
    send_newsletter()

if not name:           # empty string → False, not flips it
    ask_for_name()

if count == 0:
    show_empty_state()

if user is None:
    redirect_to_login()
PLAIN ENGLISH

 

 

 

 

 

if users: → "if users has anything in it" — no .length check needed

 

 

if not name: → "if name is empty or blank" — not flips falsy to True

 

 

if count == 0: → explicit comparison for clarity with numbers

 

 

if user is None: → always use is (not ==) to check for None

TERMS truthy falsy boolean

If/Elif/Else — First Match Wins

Python checks each condition in order. When it finds a True one, it runs that block and skips everything else.

📨
Request
🔑
if check
📝
elif check
🚪
else
Click "Next Step" to begin
💡
The ternary expression

The ternary expression is value_if_true if condition else value_if_false. So label = 'pass' if score >= 60 else 'fail' on one line. Claude uses this often for simple assignments.

TERMS elif ternary expression

Match/Case — Clean Routing

Python 3.10 added match/case — a cleaner way to route a single value to different code paths. You will see this in Claude-generated CLI tools and API handlers.

PYTHON
def handle_command(cmd: str) -> str:
    match cmd:
        case "create":
            return create_item()
        case "update":
            return update_item()
        case "delete":
            return delete_item()
        case _:
            return f"Unknown: {cmd}"
PLAIN ENGLISH

 

match cmd: → look at cmd and find the matching case

case "create": → if cmd is exactly "create", run this block

 

 

 

 

 

case _: → wildcard: runs when nothing else matched (like else)

First matching case wins — no fallthrough

TERMS match/case case _

Dictionaries — Python Named Storage

A dictionary maps keys to values. It is the data structure Claude uses to represent almost everything structured — users, configs, API responses, form data.

🌐
Dicts are everywhere

When Claude parses an API response, loads a config file, or processes form input — it works with dicts. JSON data from web APIs becomes a Python dict the moment you load it. Understanding dicts means understanding how Python sees the outside world.

PYTHON
user = {
    "name": "Alice",
    "role": "admin",
    "score": 0
}

name   = user["name"]              # "Alice"
role   = user.get("role", "viewer")  # safe lookup
user["score"] = 10                 # update
user["email"] = "a@b.com"         # add new key
del user["score"]                  # remove
PLAIN ENGLISH

 

 

 

 

 

 

user["name"] → Look up key "name" — returns "Alice". Crashes with KeyError if key missing

user.get("role", "viewer") → Safe: if "role" missing, return "viewer" as default — never crashes

user["score"] = 10 → Set or update the value at "score"

user["email"] = "a@b.com" → Dicts grow dynamically — add new keys anytime

del user["score"] → Remove the key and its value entirely

TERMS dictionary key value .get()

Nested Dicts and Comprehensions

Real-world data is rarely flat. APIs return nested dicts. Dict comprehensions let you transform key-value pairs compactly.

1
API returns:

{"user": {"name": "Alice", "address": {"city": "NYC"}}}

2
data["user"] → the inner user dict
3
data["user"]["name"] → "Alice"
4
data["user"]["address"]["city"] → "NYC"
5
Safer chained:

data.get("user", {}).get("city", "unknown")

PYTHON
scores = {"Alice": 92, "Bob": 45, "Carol": 88, "Dave": 30}

passing = {
    name: score
    for name, score in scores.items()
    if score >= 60
}
# Result: {"Alice": 92, "Carol": 88}
PLAIN ENGLISH

 

 

 

scores.items() → Loop through both keys AND values — gives pairs like ("Alice", 92)

for name, score in ... → Unpack each pair into two variables

if score >= 60 → Only include this pair if condition is true

 

Pattern: {key: value for k, v in d.items() if condition}

💡
Dict comprehensions mirror list comprehensions

Just use {} and include key: value. When Claude filters a dict or transforms all values, this is the pattern.

TERMS nested dict dict comprehension .items()

Check Your Understanding

You want to check if an optional "discount" field exists in a user dict. If missing, treat it as 0. Which code is correct?

Trace this code — what does it print?
x = 5
if x > 10: print("big")
elif x > 3: print("medium")
elif x > 1: print("small")
else: print("tiny")

You are processing commands: create, update, delete, status. Each needs different code. Which Python structure is cleanest?

03

Keeping Things Moving

Loops, functions, scope — the engine that runs every Python program

While Loops — Keep Going Until

A while loop repeats a block of code as long as a condition is True. Unlike a for loop (which counts through a collection), a while loop keeps going until told to stop.

🔍
Condition
⚙️
Loop Body
🚪
break
⏭️
continue
Click "Next Step" to begin
⚠️
Infinite loops

Infinite loops happen when the condition never becomes False and there is no break. Python runs forever until you force-stop it. Always make sure your loop has an exit condition.

PYTHON
count = 0
while count < 5:
    if count == 3:
        count += 1
        continue   # skip printing 3
    print(count)
    count += 1
PLAIN ENGLISH

 

while count < 5: → Keep repeating while count is less than 5

if count == 3: → Special case: when count hits 3

count += 1 → Increment first (so count moves past 3)

continue → Skip rest of this iteration, jump back to the condition

print(count) → Normal path: print the number

count += 1 → Increment counter (Python has no ++ operator)

TERMS while loop break continue infinite loop

The Input Loop Pattern

The while True + break pattern is in almost every interactive script Claude writes. It keeps asking for input until the user decides to stop.

PYTHON
tasks = []
while True:
    user_input = input("Command: ").strip()
    if not user_input:
        continue
    if user_input == "quit":
        break
    tasks.append(user_input)
    print(f"Added: {user_input}")
PLAIN ENGLISH

 

while True: → Run forever (intentional) — break controls exit

.strip() → Remove leading/trailing spaces from user input

if not user_input: continue → User pressed Enter without typing — skip and ask again

 

if user_input == "quit": break → Exit condition: user typed "quit"

 

tasks.append(user_input) → Add their input to our list

 

💡
while True + break — Python's do-while

while True + break is Python's version of a do-while loop — something many other languages have built-in but Python does not. When you see this pattern in Claude code, it means: run at least once, stop when the condition inside says to.

TERMS input()

Functions — Packaging Work

A function wraps a block of code into a named unit. Define it once, call it anytime. This is how Claude reuses logic across your program.

📞
Caller
📋
Function Def
⚙️
Process
📤
Return
Click "Next Step" to begin
💡
return vs print

A function with no return statement gives back None. print() shows text on screen — it does not return anything. return sends a value to the caller. These are different operations.

PYTHON
def calculate_discount(price: float, pct: float = 0.1) -> float:
    """Apply a discount to a price."""
    return price * (1 - pct)

sale  = calculate_discount(100)          # uses default pct=0.1
promo = calculate_discount(100, 0.25)    # pct=0.25
named = calculate_discount(price=200, pct=0.15)
PLAIN ENGLISH

def → Define a function. Everything indented below belongs to it

price: float → Type hint: this expects a float. Optional label, not enforced

pct: float = 0.1 → Default parameter: if not provided, use 0.1

-> float → Type hint: this function returns a float

 

return price * (1 - pct) → Compute result and send it back to the caller

 

 

 

Calling with price=200 → Keyword argument: named by parameter name

TERMS function parameter argument return value type hint default parameter

Args, Kwargs — Flexible Functions

*args collects unlimited extra positional arguments into a tuple. **kwargs collects unlimited keyword arguments into a dict. Claude uses these to build flexible functions.

PYTHON
def log_event(level: str, message: str, *tags, **metadata):
    print(f"[{level}] {message}")
    if tags:
        print(f"  Tags: {', '.join(tags)}")
    if metadata:
        print(f"  Data: {metadata}")

log_event("INFO", "User signed in")
log_event("ERROR", "Payment failed", "billing", "stripe",
          user_id=42, amount=99.99)
PLAIN ENGLISH

level: str, message: str → Two required positional parameters

 

*tags → Collect any extra positional arguments into a tuple called tags

 

**metadata → Collect any extra keyword arguments into a dict called metadata

 

 

First call: no tags, no metadata — both are empty

Second call: "billing" and "stripe" go into tags tuple; user_id and amount go into metadata dict

 

positional args Required values passed in order: func(100, 0.25)
keyword args Named when calling: func(price=100)
default args Have fallback values: pct: float = 0.1
*args Catches unlimited extra positional args as a tuple
**kwargs Catches unlimited keyword args as a dict
💡
Lambda — one-line functions

Lambda: a compact one-line function. double = lambda x: x * 2 is equivalent to a 3-line def. Claude uses lambdas for sort keys: sorted(users, key=lambda u: u['score']). Now you can read those lines.

TERMS *args **kwargs lambda keyword argument

Scope — What Can See What

Variables have scope — where they are visible. Python searches scopes in order: Local first, then Enclosing, then Global, then Built-in. This is called the LEGB rule.

1
Local

Variables inside the current function. Exists only while the function runs.

2
Enclosing

Outer function variables. Inner functions can read (not modify) them by default.

3
Global

Module level, the whole file. Modify with the global keyword inside a function.

4
Built-in

Python built-ins: len, print, range — always available everywhere.

PYTHON
total = 0          # global scope

def add_amount(x):
    global total   # I mean the global, not a local
    total += x

def make_counter():
    count = 0      # enclosing scope

    def increment():
        nonlocal count  # modify the enclosing count
        count += 1
        return count

    return increment
PLAIN ENGLISH

total = 0 at module level → global variable, visible everywhere in the file

 

 

global total → Without this, total += x would create a new local variable and crash

 

 

 

count = 0 inside make_counter → local to make_counter, enclosing scope for increment

 

 

nonlocal count → Modify the enclosing count, not create a new local one

 

 

 

return increment → Return the inner function itself — used as a callable

TERMS scope LEGB rule global nonlocal

Check Your Understanding

Claude wrote this loop. What does it print?
i = 0
while i < 4:
    i += 1
    if i == 2:
        continue
    print(i)

You want to keep asking the user for input until they type "done." Which loop pattern is correct?

You see tasks.sort(key=lambda t: t["due_date"]) in Claude code. What does it do?

04

Building with Objects

Classes, inheritance, dataclasses — how Python bundles data and behavior

Classes — Blueprints for Objects

A class is a cookie cutter. Each instance is a cookie — same shape, different frosting.

Every library Claude uses — pandas, FastAPI, Pydantic — is built from classes. Reading a class stops feeling mysterious once you know this pattern.

PYTHON
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
        self.role = "viewer"    # default

    def greet(self) -> str:
        return f"Hello, {self.name}!"

alice = User("Alice", "alice@co.com")
print(alice.greet())   # "Hello, Alice!"
PLAIN ENGLISH

class User: → Define a blueprint named User

def __init__(self, name, email): → Constructor: runs automatically when User(...) is called

self.name = name → Store name on this specific instance

 

self.role = "viewer" → Default attribute — all new Users start as viewers

 

def greet(self): → Instance method — self is always first, always means "this object"

 

alice = User("Alice", ...) → Create one instance. Python passes alice as self automatically

alice.greet() → Calls greet on alice specifically

TERMS class instance __init__ self attribute method

Inheritance — Is-A Relationships

A child class inherits everything from its parent. You only define what is different or new. super() calls the parent version of a method.

📦
Product
🏋️
PhysicalProduct
💾
DigitalProduct
Click "Next Step" to begin
PYTHON
class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

    def get_info(self) -> str:
        return f"{self.name}: ${self.price:.2f}"

class PhysicalProduct(Product):
    def __init__(self, name, price, weight_kg: float):
        super().__init__(name, price)  # run Product setup first
        self.weight_kg = weight_kg

    def shipping_cost(self) -> float:
        return self.weight_kg * 2.5
PLAIN ENGLISH

 

 

 

 

 

 

 

class PhysicalProduct(Product): → PhysicalProduct IS-A Product. Inherits everything Product has

 

super().__init__(name, price) → Run the parent (Product) constructor first, then our extra setup

self.weight_kg = weight_kg → Add our own attribute after the parent is initialized

 

def shipping_cost(self): → New method only PhysicalProduct has — Product does not know about it

A PhysicalProduct can call both get_info() (from Product) and shipping_cost() (its own)

💡
Library code is a class hierarchy

Every library Claude pulls in is a class hierarchy. A pandas DataFrame, a FastAPI HTTPException, a Pydantic BaseModel — all classes with inheritance. Understanding this means library code stops being magic.

TERMS inheritance super() override child class parent class

Dataclasses — Less Boilerplate

@dataclass auto-generates the __init__, __repr__, and __eq__ methods from your field annotations. One decorator replaces 15 lines of boilerplate.

⚠️

Without @dataclass

Write __init__ manually (8+ lines), __repr__ for printing (3 lines), __eq__ for comparisons (3 lines). Total: 15+ lines per class.

With @dataclass

Just list your fields with type annotations. The decorator generates everything else automatically.

PYTHON
from dataclasses import dataclass

@dataclass
class OrderItem:
    product_name: str
    quantity: int
    unit_price: float

    @property
    def total(self) -> float:
        return self.quantity * self.unit_price

item = OrderItem("Widget", 3, 9.99)
print(item.total)  # 29.97 (no parentheses!)
print(item)        # OrderItem(product_name='Widget', quantity=3, unit_price=9.99)
PLAIN ENGLISH

 

 

@dataclass → Decorator that auto-generates __init__, __repr__, and __eq__ from field annotations

 

product_name: str → Field declaration — becomes self.product_name in the auto-generated __init__

 

 

 

@property → Makes total behave like an attribute, not a method — call as item.total, not item.total()

 

 

 

print(item.total) → No parentheses! @property makes it look like a plain attribute

print(item) → Dataclass auto-generates a nice readable representation

⚠️
Mutable defaults are a trap

Never use mutable defaults like tags: list = [] in a dataclass — the same list object is shared between ALL instances. Use field(default_factory=list) instead. Claude sometimes gets this wrong too.

TERMS decorator @dataclass @property field annotation

Dunder Methods — Python Secret Language

Methods named with double underscores (__like_this__) are called automatically by Python when you use operators or built-in functions. They let your custom objects behave like native Python types.

Python Operations → Dunder Methods
print(obj)
len(obj)
obj1 == obj2
obj + other
item in obj
for x in obj
Click any operation to see which method it calls
PYTHON
class Cart:
    def __init__(self):
        self._items = []

    def add(self, item: str):
        self._items.append(item)

    def __len__(self) -> int:
        return len(self._items)

    def __str__(self) -> str:
        return f"Cart with {len(self)} items"

    def __contains__(self, item: str) -> bool:
        return item in self._items

cart = Cart()
cart.add("apple")
cart.add("bread")
print(len(cart))         # 2
print(cart)              # "Cart with 2 items"
print("apple" in cart)   # True
PLAIN ENGLISH

 

 

 

 

 

 

__len__ → len(cart) calls this automatically. Return the integer size

 

 

__str__ → print(cart) calls this. Return a human-readable string

 

 

__contains__ → "apple" in cart calls this. Return True or False

 

 

 

 

 

These hooks make Cart feel like a native Python type

💡
Dunders demystified

The double underscores are called dunders (double underscores). You say __str__ as dunder-str. When Claude generates a class with __repr__ or __post_init__, these are the same mechanism — hooking into Python operations.

TERMS dunder method __str__ __eq__ __len__

Check Your Understanding

Two Product instances should be equal if they have the same sku field. Which dunder method do you implement?

class Dog(Animal): — in __init__, Dog calls super().__init__(name). What does this do?

Match each concept to its description:

@dataclass
@property
super()
__init__

Auto-generates constructor and repr from field annotations

Drop here

Makes a method look like a plain attribute — no parentheses needed

Drop here

Reference to the parent class for calling parent methods

Drop here

The constructor — runs automatically when a new instance is created

Drop here
05

The Outside World

Files, JSON, exceptions — how Python reads from and writes to the world beyond your script

Files — The Library Borrowing System

A context manager is like a library borrowing system — the book is checked out, used, and automatically returned when you are done. Even if you forget. Even if something goes wrong.

📄
Your Code
🔑
__enter__
📂
File Open
🔒
__exit__
Click "Next Step" to begin
PYTHON
from pathlib import Path

data_file = Path("tasks.txt")

# Write
with open(data_file, "w") as f:
    f.write("Buy milk\n")
    f.write("Call Alice\n")

# Read
with open(data_file, "r") as f:
    for line in f:
        print(line.strip())
PLAIN ENGLISH

 

 

Path("tasks.txt") → A cross-platform path object — works the same on Mac, Windows, Linux

 

 

open(data_file, "w") → Open for writing — creates if not exists, overwrites if it does

with ... as f: → Context manager: f is the open file. When the block ends, f closes automatically

f.write("text\n") → Write text. \n is a newline character

 

 

open(data_file, "r") → Open for reading. "r" is the default mode

 

for line in f: → Iterate through the file line by line — memory efficient

💡

Almost every practical script Claude writes for you touches files. The with open(...) pattern guarantees the file closes cleanly — no leaked file handles, no data corruption. Even if your code raises an exception inside the block.

Glossary: context manager · with statement · pathlib.Path · file mode

JSON — Python Meets the Web

JSON is how data travels on the internet. Python dicts and JSON look almost identical. The json module converts between them instantly.

💡

When Claude writes code to handle an API response, every response is JSON. The moment you call response.json(), Python converts the JSON text to a Python dict — and you can access it with all the dict skills from Module 2.

PYTHON
import json

user = {
    "name": "Alice",
    "score": 95,
    "tags": ["admin", "active"]
}

# Python dict → JSON string
json_str = json.dumps(user, indent=2)

# JSON string → Python dict
loaded = json.loads(json_str)

# Write to file
with open("user.json", "w") as f:
    json.dump(user, f, indent=2)

# Read from file
with open("user.json", "r") as f:
    data = json.load(f)
PLAIN ENGLISH

 

 

 

 

 

 

 

 

json.dumps(user) → "dump to string" — converts Python dict to a JSON-formatted string

indent=2 → Pretty-print with 2-space indentation — readable to humans and tools

 

json.loads(json_str) → "load from string" — converts JSON string back to Python dict

 

 

 

json.dump(user, f) → Write JSON directly to an open file (no intermediate string)

 

 

 

json.load(f) → Read JSON directly from a file — returns a Python dict

📡

1. API response arrives

Raw JSON text comes back from the server as a string.

🔄

2. json.loads() converts

JSON text becomes a Python dict you can work with.

🔑

3. Access data

data["user"]["name"] — normal dict access.

✏️

4. Modify as dict

Update values using all your dict skills from Module 2.

📤

5. json.dumps() converts back

Python dict becomes JSON text for storage or sending.

Glossary: JSON · json.dumps() · json.loads() · serialization

Try/Except — Expecting the Unexpected

External resources fail. Files go missing. APIs go down. try/except lets your program handle these failures gracefully instead of crashing.

🎯
try block
🛡️
except block
else block
🔁
finally block
Click "Next Step" to begin
PYTHON
try:
    with open("config.json", "r") as f:
        config = json.load(f)
except FileNotFoundError:
    config = {"debug": False, "timeout": 30}
    print("Config not found — using defaults")
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")
    config = {}
else:
    print("Config loaded successfully")
finally:
    print("Setup complete")   # always runs
PLAIN ENGLISH

try: → Attempt this code — it might fail

 

 

except FileNotFoundError: → If the file does not exist, handle it here (no crash)

 

 

except json.JSONDecodeError as e: → If JSON is malformed, handle it here. e holds error details

 

 

else: → Runs ONLY if the try block completed with no exception

 

finally: → Runs no matter what — success, failure, or any exception. Use for cleanup

Glossary: exception · try/except · FileNotFoundError · finally

The Exception Hierarchy

Python exceptions form a tree. Catching a parent type catches all its children. Knowing the hierarchy helps you write precise error handling.

Python Exception Hierarchy (click to explore)
🌳
BaseException
📦
Exception
ValueError
🔀
TypeError
🗝️
KeyError
📂
FileNotFoundError
📋
IndexError
🔍
AttributeError
Click any exception to learn when it appears
🔗

Custom exceptions let you create domain-specific error types. A payment app might define PaymentDeclined(ValueError). Claude often generates these in larger applications — now you know they are just subclasses of Exception.

Glossary: exception hierarchy · ValueError · TypeError · KeyError

Check Your Understanding

Claude wrote code with user["premium_tier"] from an API response. Some users do not have this field. What is the safest fix?

Trace this code. When the file does not exist, which blocks run?
try:
    f = open("missing.txt")
    data = f.read()
except FileNotFoundError:
    data = "default"
else:
    print("File loaded")
finally:
    print("Done")

You want to append new log entries without overwriting old ones. Which file mode?

06

Trusting Your Code

pytest, fixtures, parametrize, mocking — how to verify your code works (and catch when Claude gets it wrong)

Why Tests Exist

Tests are a ratchet wrench — each green test is a click that locks in your progress. You cannot accidentally slip backward.

💡

When Claude generates tests alongside code, it is creating a record of intended behavior. The test says: this function should do X. If anyone changes the function and breaks X, the test fails loudly. Tests are a contract written in executable form.

Glossary: test · regression · pytest · test suite

Writing Assertions

A pytest test is any function whose name starts with test_. Inside, you use assert to state what you expect. If the assertion fails, pytest tells you exactly what the actual value was.

1
File naming

pytest automatically discovers files named test_*.py or *_test.py

2
Function naming

Any function starting with test_ is a test. No class or registration needed.

3
assert

assert expected_value == actual_value. If False, pytest shows both values.

4
pytest.raises()

Wrap code that SHOULD raise an exception. The test fails if no exception is raised.

PYTHON
# test_calculator.py
from calculator import add, divide

def test_add_positive_numbers():
    result = add(3, 4)
    assert result == 7

def test_divide_by_zero_raises():
    import pytest
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_add_returns_correct_type():
    result = add(1.5, 2.5)
    assert isinstance(result, float)
    assert result == 4.0
PLAIN ENGLISH

 

 

 

def test_add_positive_numbers(): → Any function starting with test_ is picked up by pytest automatically

 

assert result == 7 → "I expect result to equal 7. If not, stop and show me what it actually was."

 

def test_divide_by_zero_raises(): → A test specifically for error-handling behavior

 

with pytest.raises(ZeroDivisionError): → "I expect this code to raise ZeroDivisionError. No exception = test failure."

 

 

def test_add_returns_correct_type(): → A separate test for the return type

 

assert isinstance(result, float) → Check the type, not just the value

 

⚠️

Common trap: tests that pass but test nothing. def test_something(): result = run(); assert result — this only checks that result is truthy. A real assertion checks the specific expected value: assert result == expected.

Glossary: assert · pytest.raises() · assertion · test discovery

Fixtures — Shared Setup

Fixtures eliminate repeated setup code. Write the setup once, mark it with @pytest.fixture, and pytest injects it automatically into every test that requests it.

🔵
Test A
🟢
Test B
🟡
Test C
Fixture
Click "Next Step" to begin
PYTHON
import pytest
from app import User, is_admin

@pytest.fixture
def sample_user():
    return User(name="Alice", role="admin", active=True)

def test_user_is_active(sample_user):
    assert sample_user.active is True

def test_admin_check(sample_user):
    assert is_admin(sample_user) is True

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_add(a, b, expected):
    from calculator import add
    assert add(a, b) == expected
PLAIN ENGLISH

 

 

 

@pytest.fixture → Marks a function as a fixture. pytest calls it and passes the return value to any test that lists it as a parameter

 

 

 

def test_user_is_active(sample_user): → pytest sees sample_user in the signature and runs the fixture automatically

 

 

 

 

@pytest.mark.parametrize("a,b,expected", [...]) → Run this test multiple times with different inputs — no copy-pasting

 

 

 

 

The table (1,2,3), (0,0,0), (-1,1,0) runs the test 3 times, once per row, producing 3 separate test results

 

🔗

Parametrize is how Claude writes efficient tests. Instead of test_add_positives, test_add_zeros, test_add_negatives — three separate functions — it writes one parametrized test. When you see that decorator, it means one function tests many cases.

Glossary: fixture · @pytest.fixture · @pytest.mark.parametrize · scope

Mocking — Stub the World

Mocking replaces real dependencies (APIs, databases, files) with fake versions during tests. Your code runs in isolation, with full control over what the fakes return.

PYTHON
from unittest.mock import patch
import pytest
from weather_app import get_weather_summary

def test_weather_sunny():
    fake_data = {"temp": 22, "condition": "sunny"}

    with patch("weather_app.fetch_weather") as mock_fetch:
        mock_fetch.return_value = fake_data
        result = get_weather_summary("Tokyo")

    assert "sunny" in result
    mock_fetch.assert_called_once_with("Tokyo")
PLAIN ENGLISH

 

 

 

 

 

 

 

patch("weather_app.fetch_weather") → Replace fetch_weather in weather_app with a fake for the duration of the with block

mock_fetch.return_value = fake_data → When your code calls the fake, it returns this value

get_weather_summary("Tokyo") → Your real function runs — but the API call is intercepted

 

assert "sunny" in result → Verify the output contains the expected content

mock_fetch.assert_called_once_with("Tokyo") → Verify the mock was called exactly once with exactly this argument

⚠️

A test that patches the wrong path silently passes for the wrong reason. Always patch where the name is USED, not where it is defined. If weather_app.py does from requests import get, patch weather_app.get, not requests.get.

Glossary: mock · patch · assert_called_once_with() · test isolation

Check Your Understanding

Claude wrote a function that calls the Stripe payment API. You want to test that it handles declined cards correctly without charging anyone. How?

In a pytest fixture that uses yield, what separates setup from teardown?

You want to run test_discount with five different price inputs. What is the most efficient approach?

You spot this test Claude generated: def test_process(): result = process_data(sample); assert result. What is wrong with it?