Index Ai Academy
Index Ai Academy · Recipes

Recipes for people learning to code

249 commands and snippets from the free lessons, each with what it does. Open the lesson and every one runs in your browser, with a preview of what it produces; change it and run it again.

Your first program Free

Comments

# Weekly report for the spring campaign
print("Spring campaign")

print("Clicks:", 1540)   # from the ads dashboard, Monday to Sunday
# print("Conversions:", 61)

From Your first program · run it in the lesson

Variables Free

campaign metrics

sends = 24000
clicks = 1920
purchases = 132

click_through_rate = clicks / sends
conversion_rate = purchases / clicks

print("CTR:", click_through_rate)
print("Conversion rate:", conversion_rate)

From Variables · run it in the lesson

Changing a variable

clicks = 120
clicks = clicks + 30   # take the current value, add 30, store the result
print(clicks)

From Variables · run it in the lesson

Types: what kind of value it is

print(type("Spring Sale"))
print(type(5000))
print(type(0.035))
print(type(True))

From Variables · run it in the lesson

Numbers and arithmetic Free

ROAS and CPA from exported text

spend_text = "2390.75"
revenue_text = "3290"
conversions_text = "47"

spend = float(spend_text)
revenue = float(revenue_text)
conversions = int(conversions_text)

roas = revenue / spend
cpa = spend / conversions

print("ROAS:", round(roas, 2))
print("CPA:", round(cpa, 2))

From Numbers and arithmetic · run it in the lesson

Order of operations

revenue = 15840
cost = 3120.5

wrong_margin = revenue - cost / revenue     # divides first, then subtracts
margin = (revenue - cost) / revenue         # what we actually meant

print(wrong_margin)
print(margin)

From Numbers and arithmetic · run it in the lesson

Powers are how you model compounding: growth that builds on itself

weekly_signups = 1000
growth = 1.05                          # 5% more than last week: 1.05 times
print(weekly_signups * growth ** 4)    # four weeks of 5% growth

From Numbers and arithmetic · run it in the lesson

round(number, digits) rounds to that many decimal places

ctr = 0.034782
print(round(ctr, 3))
print(round(ctr * 100, 1))   # as a percentage
print(round(1157.625))

From Numbers and arithmetic · run it in the lesson

Converting between text and numbers

budget_text = "5000"          # text, perhaps read from a file
budget = int(budget_text)     # now a number
print(budget + 100)

price = float("19.99")
print(price * 3)

print("Clicks: " + str(120))  # + joins text, so the number must become text first

From Numbers and arithmetic · run it in the lesson

Fractions, ratios and rates Free

Rates: making groups comparable

big_clicks = 300
big_sent = 10000
small_clicks = 120
small_sent = 2000
print("Big list:", big_clicks / big_sent)
print("Small list:", small_clicks / small_sent)

From Fractions, ratios and rates · run it in the lesson

Percent change versus percentage points

old_rate = 4.0
new_rate = 5.0
points = new_rate - old_rate
percent_change = (new_rate - old_rate) / old_rate * 100
print(points, "percentage points")
print(percent_change, "% change")

From Fractions, ratios and rates · run it in the lesson

Negative numbers and distance from zero Free

Powers, roots and powers of ten Free

Formulas: order, substitution and rearranging Free

Strings and f-strings Free

tidy a subject line and report on it

raw_subject = "   LAST CHANCE: 30% off ends tonight!   "

subject = raw_subject.strip()
softer = subject.capitalize()
preview = subject[:20]

print(f"Cleaned:   {softer}")
print(f"Length:    {len(subject)} characters")
print(f"Preview:   {preview}...")
print(f"Discount?  {'%' in subject}")

From Strings and f-strings · run it in the lesson

Campaign codes, SKUs and UTM values often pack several facts into one string

code = "SUMMER25-EMAIL"
season = code[:8]
channel = code[-5:]
print(season)
print(channel)
print(code[0], code[-1])

From Strings and f-strings · run it in the lesson

String methods

raw = "  Spring SALE Launch  "
clean = raw.strip().lower()
print(clean)
print(raw)               # the original is unchanged
print(clean.replace("sale", "event"))

From Strings and f-strings · run it in the lesson

Splitting and joining

tags = "seo, email, ads".split(", ")
print(tags)
print(" | ".join(tags))

words = "write three short headlines".split()   # no separator: split on any whitespace
print(words)

From Strings and f-strings · run it in the lesson

f-strings: text built from values

product = "Midweight Hoodie"
audience = "college students"
count = 3
prompt = f"Write {count} ad headlines for the {product}, aimed at {audience}."
print(prompt)

From Strings and f-strings · run it in the lesson

Decisions with if/else Free

flag a campaign that needs attention

name = "Retargeting - Display"
budget = 2000
spend = 1880
conversions = 88

pacing = spend / budget
cpa = spend / conversions

if pacing > 0.9 and cpa > 20:
    print(f"{name}: pause and review (spent {pacing:.0%}, CPA {cpa:.2f})")
elif pacing > 0.9:
    print(f"{name}: nearly out of budget")
else:
    print(f"{name}: on track")

From Decisions with if/else · run it in the lesson

Comparisons give True or False

spend = 1890
budget = 2000
print(spend > budget)
print(spend <= budget)
print("active" == "Active")    # text comparisons are case-sensitive

From Decisions with if/else · run it in the lesson

if: run code only when something is true

spend = 2150
budget = 2000

if spend > budget:
    print("Over budget!")
    print("Overspend:", spend - budget)

print("Check complete")

From Decisions with if/else · run it in the lesson

Comparing text

country = "  us "
subject = "Last chance: free shipping"

print(country == "US")
print(country.strip().upper() == "US")
print("free" in subject.lower())

From Decisions with if/else · run it in the lesson

Classic mistakes

total = 0.1 + 0.2
print(total == 0.3)
print(round(total, 2) == 0.3)
print(abs(total - 0.3) < 0.000001)    # abs(a - b) is how far apart a and b are: abs(3 - 5) is 2

From Decisions with if/else · run it in the lesson

Lists Free

a week of signups

signups = [42, 38, 51, 47, 60, 22, 19]   # Monday to Sunday

weekdays = signups[:5]
weekend = signups[5:]

print("Total:", sum(signups))
print("Best day:", max(signups))
print("Weekday average:", sum(weekdays) / len(weekdays))
print("Weekend average:", sum(weekend) / len(weekend))
print("High to low:", sorted(signups, reverse=True))
print("Order kept:", signups)

From Lists · run it in the lesson

Many values, in order

channels = ["search", "display", "video", "email", "social"]
daily_clicks = [412, 388, 501, 467, 590]
empty = []

print(channels)
print(len(channels))      # how many items
print(len(empty))

From Lists · run it in the lesson

As with strings, asking for a position that doesn't exist, such as channels[5] h

channels = ["search", "display", "video", "email", "social"]
print(channels[0])
print(channels[-1])
print(channels[1:3])
print(channels[:2])

From Lists · run it in the lesson

Changing a list

queue = ["Spring sale", "New arrivals"]
queue.append("Customer stories")
queue.insert(0, "Welcome email")
print(queue)

queue[1] = "Spring sale (final)"
queue.remove("New arrivals")
sent = queue.pop(0)
print("Sent:", sent)
print("Still queued:", queue)

From Lists · run it in the lesson

Asking questions about a list

daily_spend = [310.5, 288.0, 402.75, 365.2, 290.0]

print("Days:", len(daily_spend))
print("Total:", sum(daily_spend))
print("Lowest:", min(daily_spend), "Highest:", max(daily_spend))
print("Average:", round(sum(daily_spend) / len(daily_spend), 2))   # add them up, divide by how many

tags = ["seo", "email", "ads"]
print("email" in tags)          # True or False
print(tags.count("seo"))        # how many times it appears
print(tags.index("ads"))        # where it first appears

From Lists · run it in the lesson

Sorting

scores = [72, 95, 64, 88]
ranked = sorted(scores, reverse=True)
print(ranked)
print(scores)        # unchanged

scores.sort()
print(scores)        # now changed

result = scores.sort()
print(result)        # None: never write scores = scores.sort()

From Lists · run it in the lesson

Two names, one list

approved = ["Headline A", "Headline B"]
backup = approved                 # same list, two names
backup.append("Headline C")
print(approved)                   # it changed too

real_copy = approved.copy()       # a separate list with the same items
real_copy.append("Headline D")
print(approved)                   # unchanged this time
print(real_copy)

From Lists · run it in the lesson

Lists inside lists

rows = [
    ["cmp_101", "search", 5000],
    ["cmp_102", "display", 2000],
]
print(rows[1])
print(rows[1][2])

From Lists · run it in the lesson

Two more tools: + and slicing to -1

print([1] + [2, 3])
items = ["a", "b", "c", "d"]
print(items[:-1])

From Lists · run it in the lesson

Loops Free

a week of ad results

ads = [
    ["Headline A", 12000, 360],
    ["Headline B", 9500, 180],
    ["Headline C", 15000, 600],
    ["Headline D", 4000, 40],
]

total_clicks = 0
best_name = ""
best_ctr = 0
weak = []

for name, impressions, clicks in ads:
    ctr = clicks / impressions
    total_clicks += clicks
    if ctr > best_ctr:
        best_ctr = ctr
        best_name = name
    if ctr < 0.02:
        weak.append(name)

print(f"Total clicks: {total_clicks:,}")
print(f"Best: {best_name} ({best_ctr:.1%})")
print(f"Under 2% CTR: {weak}")

From Loops · run it in the lesson

Do something for every item

channels = ["search", "display", "email"]

for channel in channels:
    print("Checking", channel)

print("Done")

From Loops · run it in the lesson

Looping over rows

campaigns = [
    ["Spring Sale", 5000, 3120.5],
    ["Retargeting", 2000, 2150.0],
    ["Newsletter", 500, 120.0],
]

for name, budget, spend in campaigns:
    remaining = budget - spend
    print(f"{name}: {remaining:,.2f} left")

From Loops · run it in the lesson

Counting with range, numbering with enumerate, pairing with zip

for week in range(1, 4):
    print("Week", week)

print(list(range(0, 100, 25)))   # list() shows the numbers range produces

From Loops · run it in the lesson

while: repeat until something changes

budget = 1000
weeks = 0
while budget < 5000:
    budget = budget * 1.4      # grow 40% a week
    weeks += 1
print(weeks, "weeks to pass 5000; budget is now", round(budget))

From Loops · run it in the lesson

break and continue

campaigns = [["Search", 5000, 3120.5], ["Display", 2000, 2150], ["Email", 500, 620]]

for name, budget, spend in campaigns:
    if spend > budget:
        print("First campaign over budget:", name)
        break

emails = ["ana@shop.com", "", "ben@shop.com", "   ", "cy@shop.com"]
sent = 0
for email in emails:
    if email.strip() == "":
        continue               # skip blanks
    sent += 1
print("Sent to", sent, "addresses")

From Loops · run it in the lesson

Averages and spread Free

The median: the middle value

times = [12, 3, 7, 30, 5]
in_order = sorted(times)
print(in_order)
middle = len(in_order) // 2
print(in_order[middle])

From Averages and spread · run it in the lesson

Spread: how far from the middle?

values = [38, 40, 42, 40]
mean = sum(values) / len(values)
total = 0
for v in values:
    total = total + (v - mean) ** 2
std = (total / len(values)) ** 0.5
print(mean, round(std, 2))

From Averages and spread · run it in the lesson

Coordinates, slopes and reading a chart Free

Dictionaries Free

an LLM chat request

request = {
    "model": "claude-opus-5-5",
    "max_tokens": 300,
    "messages": [
        {"role": "user", "content": "Suggest a subject line for our spring sale."},
    ],
}

reply = {"role": "assistant", "content": "Spring into savings: 30% off this week"}
request["messages"].append(reply)
request["messages"].append({"role": "user", "content": "Make it shorter."})

print(len(request["messages"]), "messages")
print("Last speaker:", request["messages"][-1]["role"])
print("First answer:", request["messages"][1]["content"])
print("System prompt:", request.get("system", "(none)"))

From Dictionaries · run it in the lesson

Look things up by name

campaign = {
    "name": "Spring Sale",
    "channel": "search",
    "budget": 5000,
    "spend": 3120.5,
}

print(campaign["name"])
print(campaign["budget"] - campaign["spend"])
print(len(campaign))     # number of key-value pairs

From Dictionaries · run it in the lesson

Adding, changing and removing

campaign = {"name": "Spring Sale", "budget": 5000, "status": "paused"}

campaign["status"] = "active"       # key exists: replace the value
campaign["clicks"] = 5470           # new key: add it
campaign["budget"] += 500           # update using the current value

del campaign["name"]                # remove a key and its value
status = campaign.pop("status")     # remove it and get the value back

print(campaign)
print(status)

campaign.update({"spend": 3120.5, "clicks": 5600})   # add or replace several at once
print(campaign)

From Dictionaries · run it in the lesson

When a key might be missing

lead = {"email": "dana@example.com", "source": "webinar"}
print("phone" in lead)
print(lead.get("phone"))
print(lead.get("phone", "unknown"))
print(lead.get("source", "unknown"))

From Dictionaries · run it in the lesson

Keys, values and items

spend = {"search": 3120.5, "display": 1890.0, "email": 120.0}

print(list(spend.keys()))
print(list(spend.values()))
print(list(spend.items()))

print("Total:", sum(spend.values()))
print("Channels A-Z:", sorted(spend))     # sorting a dict sorts its keys

From Dictionaries · run it in the lesson

Nested data

report = {
    "account": "Index Demo Shop",
    "campaigns": [
        {"name": "Spring Sale", "tags": ["search", "seasonal"], "clicks": 5470},
        {"name": "Retargeting", "tags": ["display"], "clicks": 2050},
    ],
}

print(report["campaigns"][1]["name"])       # 2nd campaign's name
print(report["campaigns"][0]["tags"][-1])   # 1st campaign's last tag
print(len(report["campaigns"]))

From Dictionaries · run it in the lesson

Reading API replies safely

full = {"content": "Spring into savings", "usage": {"input_tokens": 41, "output_tokens": 9}}
short = {"content": "Spring into savings"}

print(full.get("usage", {}).get("output_tokens", 0))
print(short.get("usage", {}).get("output_tokens", 0))

From Dictionaries · run it in the lesson

A pattern worth knowing: counting

words = ["sale", "boots", "sale", "coat", "sale", "boots"]

counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1

print(counts)

From Dictionaries · run it in the lesson

Walking through nested data

order = {"id": 7, "items": [{"sku": "TENT", "qty": 1}, {"sku": "MUG", "qty": 2}]}

print(order["items"])         # the list
print(order["items"][1])      # its second item
print(order["items"][1]["sku"])

From Dictionaries · run it in the lesson

Functions Free

scoring subject lines

def word_count(text):
    return len(text.split())

def score_subject(subject):
    """Score a subject line from 0 to 3 using simple rules."""
    score = 0
    if word_count(subject) <= 7:
        score += 1
    if "%" in subject:
        score += 1
    if subject != subject.upper():
        score += 1
    return score

subjects = [
    "Last chance: 30% off everything",
    "OUR BIGGEST SALE OF THE YEAR STARTS NOW FOR EVERYONE",
    "New arrivals you'll love",
]
for s in subjects:
    print(score_subject(s), "-", s)

From Functions · run it in the lesson

Packaging steps under a name

def click_through_rate(clicks, impressions):
    return clicks / impressions

print(click_through_rate(5470, 182340))
print(click_through_rate(2050, 410200))

From Functions · run it in the lesson

return versus print

def total_with_print(a, b):
    print(a + b)

def total_with_return(a, b):
    return a + b

x = total_with_print(2, 3)
y = total_with_return(2, 3)
print("x is", x)
print("y is", y)

From Functions · run it in the lesson

Default values and keyword arguments

def subject_line(product, discount=10, urgent=False):
    line = f"{discount}% off {product}"
    if urgent:
        line = line + " - today only"
    return line

print(subject_line("hoodies"))
print(subject_line("hoodies", 25))
print(subject_line("hoodies", urgent=True))
print(subject_line(discount=40, product="mugs"))

From Functions · run it in the lesson

Docstrings

def roas(revenue, spend):
    """Return on ad spend: revenue earned per unit of currency spent. Returns 0 if spend is 0."""
    if spend == 0:
        return 0
    return revenue / spend

print(roas.__doc__)

From Functions · run it in the lesson

Defining isn't running

def double(n):
    print(n * 2)        # shows it, but gives nothing back

answer = double(5)
print(answer)

From Functions · run it in the lesson

While loops Free

climbing piles of crates

from robot import *

def turn_right():
    turn_left()
    turn_left()
    turn_left()

def climb_over():
    turn_left()
    while not right_is_clear():
        move()
    turn_right()
    move()
    move()
    turn_right()
    while front_is_clear():
        move()
    turn_left()

world("stacks")
while not at_goal():
    if front_is_clear():
        move()
    else:
        climb_over()

From While loops · run it in the lesson

When you can't count the repeats

savings = 0
weeks = 0
while savings < 500:
    savings = savings + 60
    weeks = weeks + 1

print("weeks needed:", weeks)
print("saved:", savings)

From While loops · run it in the lesson

Finding bugs when nothing crashes Free

Functions as values Free

a shop assistant's tools

STOCK = {"tent": 4, "mug": 0, "stove": 9}
PRICES = {"tent": 289, "mug": 14, "stove": 65}

def in_stock(item):
    return STOCK.get(item, 0) > 0

def price_of(item):
    return PRICES.get(item)

TOOLS = {"in_stock": in_stock, "price_of": price_of}

def call_tool(name, **inputs):
    if name not in TOOLS:
        return {"error": f"No tool called {name}"}
    return TOOLS[name](**inputs)

print(call_tool("price_of", item="stove"))
print(call_tool("in_stock", item="mug"))
print(call_tool("refund", order=12))

From Functions as values · run it in the lesson

A table of functions

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

OPERATIONS = {"+": add, "-": subtract, "*": multiply}

operation = OPERATIONS["*"]
print(operation(6, 7))
print(OPERATIONS["-"](10, 4))

From Functions as values · run it in the lesson

Handing a function to another function: key=

products = [
    {"name": "Ridgeline tent", "price": 289},
    {"name": "Trail mug", "price": 14},
    {"name": "Camp stove", "price": 65},
]

def price_of(product):
    return product["price"]

cheapest_first = sorted(products, key=price_of)
for p in cheapest_first:
    print(p["name"])
print(max(products, key=price_of)["name"])

From Functions as values · run it in the lesson

Small functions on one line: lambda

products = [
    {"name": "Ridgeline tent", "price": 289, "rating": 4.6},
    {"name": "Trail mug", "price": 14, "rating": 4.8},
    {"name": "Camp stove", "price": 65, "rating": 4.2},
]

by_rating = sorted(products, key=lambda p: p["rating"], reverse=True)
for p in by_rating:
    print(p["name"])

From Functions as values · run it in the lesson

map and filter, and why comprehensions usually win

prices = [289, 14, 65, 120]

with_tax = list(map(lambda p: round(p * 1.08, 2), prices))
under_100 = list(filter(lambda p: p < 100, prices))
print(with_tax)
print(under_100)

From Functions as values · run it in the lesson

Any number of arguments: args and kwargs

def order_total(*prices, discount=0.0):
    return round(sum(prices) * (1 - discount), 2)

print(order_total(289, 14))
print(order_total(289, 14, 65, discount=0.1))

def describe(name, **details):
    parts = []
    for key, value in details.items():
        parts.append(f"{key}: {value}")
    return name + " (" + ", ".join(parts) + ")"

print(describe("Ridgeline tent", weight="2.1 kg", sleeps=2))

From Functions as values · run it in the lesson

Comprehensions Free

cleaning a lead list

raw_emails = ["  Ana@Shop.com", "ben@mail.com ", "", "CARA@site.io", "not-an-email", "ben@mail.com"]

cleaned = [e.strip().lower() for e in raw_emails]
valid = [e for e in cleaned if "@" in e]
unique = sorted(dict.fromkeys(valid))

print(len(raw_emails), "raw ->", len(unique), "usable")
print(unique)

From Comprehensions · run it in the lesson

The loop you keep writing

emails = ["Ana@Shop.com", "BEN@mail.com", "cara@site.io"]

clean = []
for e in emails:
    clean.append(e.lower())

print(clean)

From Comprehensions · run it in the lesson

Filtering with if

campaigns = ["Spring Sale - Search", "Retargeting - Display", "Competitor Terms - Search", "Newsletter Promo - Email"]
search = [name for name in campaigns if name.endswith("Search")]
print(search)

spend = [3120.5, 1890, 6400, 120, 0, 2390.75]
big = [s for s in spend if s >= 2000]
print(big)

From Comprehensions · run it in the lesson

Looping over a dictionary

budgets = {"search": 5000, "display": 2000, "video": 8000, "email": 500}

for channel in budgets:
    print(channel, budgets[channel])

From Comprehensions · run it in the lesson

Dictionary comprehensions

products = ["tee", "hoodie", "mug"]
name_lengths = {p: len(p) for p in products}
print(name_lengths)

From Comprehensions · run it in the lesson

Comprehensions with sum, max and len

campaigns = [
    {"name": "Spring Sale", "spend": 3120.5, "revenue": 15840},
    {"name": "Retargeting", "spend": 1890, "revenue": 5236},
    {"name": "Brand Video", "spend": 6400, "revenue": 2870},
]
total_spend = sum([c["spend"] for c in campaigns])
profitable = [c["name"] for c in campaigns if c["revenue"] > c["spend"]]
best_revenue = max([c["revenue"] for c in campaigns])

print(total_spend)
print(profitable)
print(best_revenue)

From Comprehensions · run it in the lesson

Where the if goes

prices = [5, 20, 50]

print([p for p in prices if p > 10])                  # keep some items
print(["big" if p > 10 else "small" for p in prices]) # choose a value for every item

From Comprehensions · run it in the lesson

Sets: unique items and fast lookups Free

two numbers that add up to a target

def pair_for(prices, budget):
    seen = set()
    for p in prices:
        if budget - p in seen:
            return (budget - p, p)
        seen.add(p)
    return None

prices = [45, 170, 12, 39, 88, 61]
print(pair_for(prices, 100))   # 39 + 61
print(pair_for(prices, 1000))  # no pair

From Sets: unique items and fast lookups · run it in the lesson

A collection with no repeats

gear = {"tent", "stove", "lantern"}
print(gear, len(gear))

gear.add("stove")   # already there: nothing happens
gear.add("rope")
print(gear, len(gear))

signups = ["ana@ex.org", "bo@ex.org", "ana@ex.org", "cy@ex.org", "bo@ex.org"]
print(len(signups), "sign-ups from", len(set(signups)), "people")

From Sets: unique items and fast lookups · run it in the lesson

Comparing collections

saturday = {"Ana", "Bo", "Cy", "Dee"}
sunday = {"Cy", "Dee", "Eli"}

print("both days:", sorted(saturday & sunday))       # intersection
print("either day:", sorted(saturday | sunday))      # union
print("Saturday only:", sorted(saturday - sunday))   # difference
print("just one day:", sorted(saturday ^ sunday))    # symmetric difference

print({"Cy", "Dee"} <= saturday)        # subset: is every one of them on Saturday?
print(saturday.isdisjoint({"Fay"}))    # no one in common

From Sets: unique items and fast lookups · run it in the lesson

Reconciling two lists

yesterday = ["TE-101", "ST-114", "la-102 ", "BO-120"]
today = ["TE-101", "LA-102", "PA-111", "ST-114", "TE-101"]

def clean(skus):
    return {s.strip().upper() for s in skus}

before, after = clean(yesterday), clean(today)
print("added:", sorted(after - before))
print("removed:", sorted(before - after))
print("kept:", sorted(before & after))

From Sets: unique items and fast lookups · run it in the lesson

Errors and exceptions Free

messy survey scores

answers = ["8", "10", "seven", "", "9", "11", "6"]

def parse_score(text):
    score = int(text)
    if score < 0 or score > 10:
        raise ValueError(f"{score} is outside 0-10")
    return score

scores = []
problems = []
for a in answers:
    try:
        scores.append(parse_score(a))
    except ValueError as e:
        problems.append(repr(a) + ": " + str(e))

print("Valid scores:", scores)
print("Average:", round(sum(scores) / len(scores), 2))
for p in problems:
    print("Skipped", p)

From Errors and exceptions · run it in the lesson

Handling errors with try and except

raw_values = ["120", "95", "n/a", "300", ""]

total = 0
skipped = 0
for raw in raw_values:
    try:
        total += int(raw)
    except ValueError:
        skipped += 1

print("Total clicks:", total)
print("Skipped:", skipped)

From Errors and exceptions · run it in the lesson

else and finally

def parse_budget(text):
    try:
        amount = float(text)
    except ValueError:
        print("Not a number:", text)
        return None
    else:
        print("Parsed", amount)
        return amount
    finally:
        print("Done checking", repr(text))

parse_budget("2500")
parse_budget("lots")

From Errors and exceptions · run it in the lesson

Don't catch everything

prices = [28, 64, 18]
try:
    total = sum(prices)
    average = total / len(prise)   # typo: should be prices
except Exception:
    average = 0
print(average)

From Errors and exceptions · run it in the lesson

Modules and imports Free

a week of orders

import statistics
from collections import Counter
from datetime import date

order_values = [64, 28, 92, 18, 45, 64, 130, 22]
order_sources = ["google", "email", "google", "instagram", "email", "google", "direct", "search"]
first_order = date(2026, 11, 23)
last_order = date(2026, 11, 29)

top_source, top_count = Counter(order_sources).most_common(1)[0]

print("Orders:", len(order_values))
print("Average order value:", round(statistics.mean(order_values), 2))
print("Median order value:", statistics.median(order_values))
print("Top source:", top_source, f"({top_count} orders)")
print("Days covered:", (last_order - first_order).days + 1)

From Modules and imports · run it in the lesson

Three ways to import

import statistics
from math import floor, ceil
import datetime as dt

print(statistics.mean([12, 18, 21]))
print(floor(7.9), ceil(7.1))
print(dt.date(2026, 11, 27).strftime("%A"))

From Modules and imports · run it in the lesson

A short tour of the standard library

import random

random.seed(7)
subject_lines = ["Last chance", "New arrivals", "Your weekly picks"]
print(random.choice(subject_lines))
print(random.randint(1, 100))

From Modules and imports · run it in the lesson

The random module, in full

import random

random.seed(3)
print(random.randint(1, 6))
print(random.choice(["tent", "stove", "lamp"]))
deck = ["A", "K", "Q", "J"]
print(random.shuffle(deck), deck)
print(random.sample(range(1, 50), 3))

From Modules and imports · run it in the lesson

Classes and objects Free

Data and the functions that use it

class Campaign:
    def __init__(self, name, spend, revenue):
        self.name = name
        self.spend = spend
        self.revenue = revenue

    def roas(self):
        return self.revenue / self.spend

spring = Campaign("Spring Sale", 3120.5, 15840)
video = Campaign("Brand Video", 6400, 2870)

print(spring.name, round(spring.roas(), 2))
print(video.name, round(video.roas(), 2))

From Classes and objects · run it in the lesson

What self is

class Campaign:
    def __init__(self, name, spend, revenue):
        self.name = name
        self.spend = spend
        self.revenue = revenue

    def roas(self):
        return self.revenue / self.spend

spring = Campaign("Spring Sale", 3120.5, 15840)
print(spring.roas())
print(Campaign.roas(spring))

From Classes and objects · run it in the lesson

A readable printout with repr

class Campaign:
    def __init__(self, name, spend):
        self.name = name
        self.spend = spend

    def __repr__(self):
        return f"Campaign(name='{self.name}', spend={self.spend})"

print(Campaign("Search", 120))
print([Campaign("Search", 120), Campaign("Email", 40)])

From Classes and objects · run it in the lesson

Inheritance and class attributes Free

three ways to ship

class Shipping:
    name = "Standard"
    def cost(self, weight):
        return 5 + weight * 0.5

class Express(Shipping):
    name = "Express"
    def cost(self, weight):
        return super().cost(weight) * 3

class Pickup(Shipping):
    name = "Store pickup"
    def cost(self, weight):
        return 0

weight = 4
for option in [Shipping(), Express(), Pickup()]:
    print(f"{option.name}: {option.cost(weight)}")

From Inheritance and class attributes · run it in the lesson

Subclassing classes you didn't write

class OutOfStock(Exception):
    pass

def reserve(item, stock):
    if stock.get(item, 0) == 0:
        raise OutOfStock(f"{item} is sold out")
    stock[item] = stock[item] - 1

stock = {"tent": 1}
try:
    reserve("tent", stock)
    reserve("tent", stock)
except OutOfStock as problem:
    print("Sorry:", problem)

From Inheritance and class attributes · run it in the lesson

Dates, times and time zones Free

Dates, datetimes and durations

from datetime import date, datetime, timedelta

launch = date(2025, 3, 14)
print(launch, launch.year, launch.strftime("%A"))

order_placed = datetime(2025, 3, 14, 9, 12)
delivered = order_placed + timedelta(days=3, hours=4)
print(delivered)
print(delivered - order_placed)
print((delivered - order_placed).total_seconds() / 3600, "hours")
print(delivered > order_placed)

From Dates, times and time zones · run it in the lesson

Dates as text

from datetime import datetime

placed = datetime(2025, 3, 14, 9, 12)
print(placed.strftime("%B %d, %Y at %I:%M %p"))
print(placed.strftime("%d/%m/%Y"))

typed = datetime.strptime("06/09/2024 14:30", "%m/%d/%Y %H:%M")
print(typed)

print(placed.isoformat())
print(datetime.fromisoformat("2025-03-14T09:12:00"))

From Dates, times and time zones · run it in the lesson

Time zones: aware and naive

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

pacific = ZoneInfo("America/Los_Angeles")
mountain = ZoneInfo("America/Denver")

opens = datetime(2025, 3, 14, 10, 0, tzinfo=pacific)
print(opens)
print(opens.astimezone(mountain))
print(opens.astimezone(timezone.utc))

now_utc = datetime.now(timezone.utc)
print(now_utc.tzinfo, now_utc.utcoffset())

From Dates, times and time zones · run it in the lesson

When the clocks change

from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

pacific = ZoneInfo("America/Los_Angeles")
saturday = datetime(2025, 3, 8, 10, 0, tzinfo=pacific)

same_time_sunday = saturday + timedelta(days=1)
print(same_time_sunday, "wall clock: 10:00 again")

exactly_24h = (saturday.astimezone(timezone.utc) + timedelta(hours=24)).astimezone(pacific)
print(exactly_24h, "24 real hours later")

From Dates, times and time zones · run it in the lesson

Storing and sending times

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

stamp = datetime(2025, 3, 14, 17, 12, tzinfo=timezone.utc).isoformat()
print(stamp)
back = datetime.fromisoformat(stamp)
print(back.astimezone(ZoneInfo("America/Denver")).strftime("%a %b %d, %I:%M %p %Z"))

From Dates, times and time zones · run it in the lesson

Regular expressions Free

an order log into records

import re

LOG = """2025-03-14 09:12:05 ORDER #50231 store=portland sku=TE-101 qty=1 total=$200.00
2025-03-14 09:13:40 heartbeat ok
2025-03-14 09:20:11 ORDER #50232 store=online sku=FO-120 qty=3 total=$60.00
2025-03-14 09:31:52 ORDER #50233 store=denver sku=PA-109 qty=1 total=$1,095.00"""

LINE = re.compile(r"""
    (?P<date>\d{4}-\d{2}-\d{2}) \s (?P<time>\d{2}:\d{2}:\d{2}) \s ORDER \s
    \#(?P<order>\d+) \s store=(?P<store>\w+) \s sku=(?P<sku>[A-Z]{2}-\d{3}) \s
    qty=(?P<qty>\d+) \s total=\$(?P<total>[\d,]+\.\d{2})
""", re.VERBOSE)

records = []
for line in LOG.splitlines():
    m = LINE.fullmatch(line)
    if not m:
        continue
    row = m.groupdict()
    row["order"] = int(row["order"])
    row["qty"] = int(row["qty"])
    row["total"] = float(row["total"].replace(",", ""))
    records.append(row)

print(len(records), "orders")
print(records[2])
print(sum(r["total"] for r in records))

From Regular expressions · run it in the lesson

A pattern for text

import re

line = "Order 50231: 2 tents, 1 stove, shipped in 3 days"
print(re.search(r"\d+", line).group())
print(re.findall(r"\d+", line))

From Regular expressions · run it in the lesson

The building blocks

import re

note = "Swap TE-101 for TE-104, and restock AP-122. Ignore te-999 and ABC-1234."
print(re.findall(r"[A-Z]{2}-\d{3}", note))
print(re.findall(r"\b[A-Z]{2}-\d{3}\b", note))

From Regular expressions · run it in the lesson

Groups: pulling out the pieces

import re

line = "2025-03-14 09:12 ORDER #50231 store=portland total=$200.00"
m = re.search(r"#(?P<order>\d+) store=(?P<store>\w+) total=\$(?P<total>[\d.]+)", line)
print(m.group("order"), m.group("store"))
print(m.groupdict())

for found in re.finditer(r"(\w+)=(\S+)", line):
    print(found.group(1), "->", found.group(2))

From Regular expressions · run it in the lesson

Replace and split

import re

email = "Hi, my order #50231 hasn't arrived. Card ending 4242. Call me on 503-555-0134."
print(re.sub(r"\d{3}-\d{3}-(\d{4})", r"***-***-\1", email))
print(re.sub(r"\s+", " ", "too    many\n  spaces"))

tags = "hiking; camping ,budget;  winter"
print(re.split(r"\s*[;,]\s*", tags))

From Regular expressions · run it in the lesson

Whole-string checks, and readable patterns

import re

for code in ["TE-101", "TE-101x", " TE-101", "te-101"]:
    print(repr(code), bool(re.fullmatch(r"[A-Z]{2}-\d{3}", code)))

From Regular expressions · run it in the lesson

JSON Free

reading a model's structured reply

import json

model_reply = '{"sentiment": "negative", "score": 0.18, "topics": ["shipping", "refund"], "needs_followup": true}'

review = json.loads(model_reply)

if review["needs_followup"]:
    print("Flag for the support team:", ", ".join(review["topics"]))
print(f"Sentiment: {review['sentiment']} (score {review['score']})")

From JSON · run it in the lesson

json.loads: from text to Python

import json

text = '{"campaign": "Spring Sale", "active": true, "budget": 5000, "channels": ["search", "email"], "owner": null}'
data = json.loads(text)

print(type(text))
print(type(data))
print(data["campaign"], data["active"], data["owner"])
print(data["channels"][1])

From JSON · run it in the lesson

json.dumps: from Python to text

import json

report = {"campaign": "Spring Sale", "roas": 5.08, "flags": [], "approved": False, "notes": None}
print(json.dumps(report))
print(json.dumps(report, indent=2))

From JSON · run it in the lesson

JSON isn't Python, even when it looks it

import json

data = {"active": True, "coupon": None}
print(str(data))
print(json.dumps(data))

From JSON · run it in the lesson

Files and CSV data Free

from export to answer

import csv

with open("email_export.csv", "w", newline="") as f:
    f.write("subject,sends,opens\n")
    f.write("Last chance: 30% off,24000,6240\n")
    f.write("New arrivals you'll love,22000,4180\n")
    f.write("Your weekly picks,23500,3290\n")

best_subject = ""
best_rate = 0
with open("email_export.csv", newline="") as f:
    for row in csv.DictReader(f):
        rate = int(row["opens"]) / int(row["sends"])
        print(f"{rate:.1%}  {row['subject']}")
        if rate > best_rate:
            best_rate = rate
            best_subject = row["subject"]

print("Best:", best_subject)

From Files and CSV data · run it in the lesson

Writing and reading a file

with open("notes.txt", "w") as f:
    f.write("Q4 plan\n")
    f.write("Raise search budget\n")
    f.write("Test new subject lines\n")

with open("notes.txt") as f:
    contents = f.read()

print(contents)

From Files and CSV data · run it in the lesson

One line at a time

with open("subjects.txt", "w") as f:
    f.write("Last chance: 30% off\n")
    f.write("New arrivals you'll love\n")
    f.write("Your weekly picks\n")

with open("subjects.txt") as f:
    for line in f:
        line = line.strip()
        print(len(line), line)

From Files and CSV data · run it in the lesson

Where is that file?

from pathlib import Path

print(Path.cwd())
Path("data").mkdir(exist_ok=True)
Path("data/orders.csv").write_text("id,total\n1,40\n")
print(Path("data/orders.csv").exists(), Path("data/orders.csv").resolve())

From Files and CSV data · run it in the lesson

Paths with pathlib

from pathlib import Path

folder = Path("exports")
folder.mkdir(exist_ok=True)
report = folder / "summary.txt"
report.write_text("ROAS up 12% week over week\n")

print(report)
print(report.exists(), report.suffix, report.name)
print(report.read_text())
for p in folder.iterdir():
    print("Found:", p.name)

From Files and CSV data · run it in the lesson

NumPy arrays Free

a week of campaign data

import numpy as np

campaigns = ["Search", "Display", "Email"]
# rows: campaigns. columns: Mon to Sun
spend = np.array([
    [450, 470, 430, 460, 480, 300, 280],
    [270, 265, 280, 270, 275, 260, 270],
    [20, 15, 25, 20, 20, 10, 10],
])
revenue = np.array([
    [2300, 2500, 2100, 2400, 2600, 1500, 1300],
    [740, 700, 800, 760, 750, 720, 766],
    [1000, 900, 1200, 950, 1100, 700, 750],
])

weekly_roas = revenue.sum(axis=1) / spend.sum(axis=1)
daily_revenue = revenue.sum(axis=0)
search_daily_roas = revenue[0] / spend[0]

for name, roas in zip(campaigns, weekly_roas.round(2)):
    print(name, roas)
print("Best day (0 = Monday):", daily_revenue.argmax())
print("Days Search beat ROAS 5:", (search_daily_roas > 5).sum())

From NumPy arrays · run it in the lesson

Shape and dtype

import numpy as np

spend = np.array([3120.5, 1890, 6400, 120])
table = np.array([
    [5470, 301],
    [2050, 88],
    [3800, 41],
])

print(spend.dtype, spend.shape)
print(table.dtype, table.shape, table.ndim)

From NumPy arrays · run it in the lesson

Indexing, slicing and masks

import numpy as np

table = np.array([
    [5470, 301],   # clicks, conversions for campaign 0
    [2050, 88],    # campaign 1
    [3800, 41],    # campaign 2
])
print(table[0])        # first row
print(table[0, 1])     # row 0, column 1
print(table[:, 0])     # every row, column 0: all the clicks
print(table[1:, 1])    # rows 1 onward, column 1

From NumPy arrays · run it in the lesson

Summaries and axes

import numpy as np

# rows: 3 campaigns. columns: clicks on Mon, Tue, Wed
daily = np.array([
    [120, 135, 150],
    [80, 60, 70],
    [45, 50, 40],
])
print(daily.sum())            # everything
print(daily.sum(axis=0))      # down the rows: one total per day
print(daily.sum(axis=1))      # across the columns: one total per campaign
print(daily.mean(axis=1).round(1))
print(daily.sum(axis=1).argmax())

From NumPy arrays · run it in the lesson

Broadcasting

import numpy as np

prices = np.array([28, 64, 18])      # shape (3,)
print(prices * 1.2)                  # 1.2 is applied to every element

units = np.array([
    [3, 1, 4],                       # day 1: tee, hoodie, mug
    [2, 5, 0],                       # day 2
])                                   # shape (2, 3)
print(units * prices)                # prices is stretched down both rows

From NumPy arrays · run it in the lesson

DataFrames with pandas Free

which categories grew?

import pandas as pd

catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv")

by_category = catalog.groupby("category")[["units_q1", "units_q2"]].sum()
by_category["growth_pct"] = ((by_category["units_q2"] / by_category["units_q1"] - 1) * 100).round(1)
print(by_category.sort_values("growth_pct", ascending=False))

From DataFrames with pandas · run it in the lesson

A spreadsheet you can program

import pandas as pd

df = pd.DataFrame({
    "name": ["Ridgeline tent", "Trail mug", "Camp stove", "Rain jacket"],
    "category": ["tents", "camp kitchen", "camp kitchen", "apparel"],
    "price": [289, 14, 65, 120],
    "rating": [4.6, 4.8, 4.2, 4.5],
})
print(df)
print(df.shape)
print(list(df.columns))

From DataFrames with pandas · run it in the lesson

Loading a CSV

import pandas as pd

with open("stock.csv", "w") as f:
    f.write("sku,item,stock,price\nTE-101,1-person tent,14,195\nPA-108,daypack 18L,0,55\nAP-122,rain jacket,31,120\n")

df = pd.read_csv("stock.csv")
print(df.head())
print(df.describe())

From DataFrames with pandas · run it in the lesson

Columns and rows

import pandas as pd

catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv")

cheap = catalog["price"] < 60
print(cheap.head(4).tolist())
print(catalog[cheap][["name", "price"]])

popular_tents = catalog[(catalog["category"] == "tents") & (catalog["rating"] >= 4.4)]
print(len(popular_tents), "well-rated tents")

From DataFrames with pandas · run it in the lesson

New columns, sorting and the top few

import pandas as pd

catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv")

catalog["margin"] = catalog["price"] - catalog["cost"]
catalog["revenue_q2"] = catalog["price"] * catalog["units_q2"]

print(catalog.sort_values("margin", ascending=False)[["name", "margin"]].head(3))
print(catalog.nlargest(3, "revenue_q2")[["sku", "revenue_q2"]])

best = catalog["margin"].idxmax()
print(catalog.loc[best, "name"])

From DataFrames with pandas · run it in the lesson

Counting and grouping

import pandas as pd

catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv")

print(catalog["category"].value_counts())
print(catalog.groupby("category")["price"].mean().round(1))

From DataFrames with pandas · run it in the lesson

Missing values, and reading one cell

import pandas as pd

catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv")

print(catalog["rating"].isna().sum(), "products have no rating")
print(catalog[catalog["rating"].isna()][["sku", "name", "reviews"]])

rated = catalog.dropna(subset=["rating"])
print(len(rated), "rated products")

print(catalog.loc[0, "name"])          # row label 0, column "name"
print(catalog.iloc[-1]["sku"])         # the last row, by position

From DataFrames with pandas · run it in the lesson

Charts with Matplotlib Free

a chart ready to share

import matplotlib.pyplot as plt
import pandas as pd

daily = pd.read_csv("https://api.gradient.test/v1/datasets/shoe-orders.csv")
daily["month"] = daily["date"].str[:7]
monthly = daily.groupby("month")["orders"].agg(["sum", "count"])
complete = monthly[monthly["count"] >= 28]
recent = complete.loc["2024-01":]

fig, ax = plt.subplots(figsize=(8, 3.5))
ax.plot(recent.index, recent["sum"], marker="o", label="orders")
ax.set_ylim(bottom=0)
ax.set_title("Trail-shoe orders peak every May to August")
ax.set_ylabel("orders per month")
ax.tick_params(axis="x", labelrotation=90)
ax.spines[["top", "right"]].set_visible(False)
fig.text(0.01, -0.02, "Source: practice shop online orders, complete months only.", fontsize=8)
print(recent["sum"].idxmax(), recent["sum"].max())

From Charts with Matplotlib · run it in the lesson

A figure and its axes

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
orders = [405, 416, 579, 735, 846, 848]

fig, ax = plt.subplots(figsize=(6, 3.5))
ax.plot(months, orders, marker="o")
ax.set_title("Trail-shoe orders, first half of 2024")
ax.set_ylabel("orders")
ax.set_ylim(bottom=0)

From Charts with Matplotlib · run it in the lesson

Scales and baselines

import matplotlib.pyplot as plt

sellers = ["A", "B", "C", "D", "E", "F"]
units = [12, 48, 150, 900, 2400, 31000]

fig, (left, right) = plt.subplots(1, 2, figsize=(10, 3.2))
left.bar(sellers, units)
left.set_title("Linear: most bars vanish")
right.bar(sellers, units)
right.set_yscale("log")
right.set_title("Log scale: every seller visible")

From Charts with Matplotlib · run it in the lesson

Box plots: comparing spreads

import matplotlib.pyplot as plt
import pandas as pd

catalog = pd.read_csv("https://api.gradient.test/v1/datasets/catalog.csv")
names = sorted(catalog["category"].unique())
groups = [catalog[catalog["category"] == c]["price"] for c in names]

fig, ax = plt.subplots(figsize=(7, 3.5))
ax.boxplot(groups, tick_labels=names)
ax.set_title("Price by category")
ax.set_ylabel("price ($)")

From Charts with Matplotlib · run it in the lesson

Project: images are arrays Free

a contact sheet

import matplotlib.pyplot as plt
import numpy as np

def make_scene(h=120, w=160):
    img = np.zeros((h, w, 3), dtype=np.uint8)
    rows = np.arange(h)[:, None]
    cols = np.arange(w)[None, :]
    img[:, :] = (150, 200, 235)
    img[h * 2 // 3:, :] = (70, 130, 60)
    img[(rows - 25) ** 2 + (cols - 130) ** 2 <= 12 ** 2] = (250, 215, 80)
    img[(rows >= 30) & (rows <= 100) & (abs(cols - 70) <= (rows - 30) * 0.8)] = (230, 120, 40)
    return img

scene = make_scene()
gray = np.round(scene @ [0.299, 0.587, 0.114]).astype(np.uint8)
gray3 = np.stack([gray, gray, gray], axis=2)
dark = np.clip(scene.astype(int) - 70, 0, 255).astype(np.uint8)
mirrored = scene[:, ::-1]

top = np.hstack([scene, gray3])
bottom = np.hstack([dark, mirrored])
sheet = np.vstack([top, bottom])
print(sheet.shape)

fig, ax = plt.subplots(figsize=(6, 4.5))
ax.imshow(sheet)
ax.axis("off")

From Project: images are arrays · run it in the lesson

A picture is a grid of numbers

import matplotlib.pyplot as plt
import numpy as np

tiny = np.array([
    [[255, 0, 0], [0, 255, 0], [0, 0, 255]],
    [[255, 255, 255], [128, 128, 128], [0, 0, 0]],
], dtype=np.uint8)

print(tiny.shape, tiny.dtype)
print(tiny[0, 2])          # row 0, column 2: the blue pixel

fig, ax = plt.subplots(figsize=(3, 2))
ax.imshow(tiny)
ax.axis("off")

From Project: images are arrays · run it in the lesson

Flip, crop and gray

import numpy as np

img = np.arange(24, dtype=np.uint8).reshape(2, 4, 3)   # a 2 x 4 image
print(img[::-1].shape)        # upside down: rows reversed
print(img[:, ::-1].shape)     # mirrored: columns reversed
print(img[:, 1:3].shape)      # a crop: columns 1 and 2
print(img[:, :, 0])           # the red channel alone: a 2 x 4 grid

From Project: images are arrays · run it in the lesson

Brighter without wrapping around

import numpy as np

pixels = np.array([250, 100, 5], dtype=np.uint8)
print(pixels + 10)                                        # wraps: 250 becomes 4

safe = np.clip(pixels.astype(int) + 10, 0, 255).astype(np.uint8)
print(safe)                                               # stops at 255
print(np.clip(pixels.astype(int) - 20, 0, 255).astype(np.uint8))   # darker stops at 0

From Project: images are arrays · run it in the lesson

A color palette

import numpy as np

img = np.zeros((4, 4, 3), dtype=np.uint8)
img[:, :] = (150, 200, 235)
img[3, :] = (70, 130, 60)
img[0, 0] = (250, 215, 80)

pixels = img.reshape(-1, 3)                    # 16 rows of (r, g, b)
colors, counts = np.unique(pixels, axis=0, return_counts=True)
order = np.argsort(counts)[::-1]               # most common first
for i in order:
    print(tuple(int(v) for v in colors[i]), counts[i])

From Project: images are arrays · run it in the lesson

The terminal Free

Version control with git Free

Packages and environments Free

Python on your own computer Free

what the self-check checks

import hashlib
import platform
import sys

version = platform.python_version()
in_venv = sys.prefix != sys.base_prefix
passed = True  # here we pretend pytest passed
fields = f"python={version} venv={'yes' if in_venv else 'no'} tests={'passed' if passed else 'failed'}"
code = hashlib.sha256(fields.encode()).hexdigest()[:8]
print(f"SETUP-CHECK {fields} code={code}")

typo = fields.replace("venv=no", "venv=yes")
print("changing one field changes the code:", hashlib.sha256(typo.encode()).hexdigest()[:8])

From Python on your own computer · run it in the lesson

Where the academy's Python runs

import platform
import sys

print("Python", platform.python_version())
print("running on:", sys.platform)
print("in a virtual environment:", sys.prefix != sys.base_prefix)

From Python on your own computer · run it in the lesson

How APIs work Free

building a request body

import json

new_campaign = {
    "name": "Summer Launch - Social",
    "budget": 1500,
    "channel": "social",
}

body = json.dumps(new_campaign)
print(body)
print(type(body))

From How APIs work · run it in the lesson

Headers and bodies

import json

body = '{"data": [{"id": "cmp_101", "name": "Spring Sale - Search", "clicks": 5470}, {"id": "cmp_104", "name": "Newsletter Promo - Email", "clicks": 1920}], "count": 2}'

result = json.loads(body)
print(type(result))
print(result["count"])
for campaign in result["data"]:
    print(campaign["name"], campaign["clicks"])

From How APIs work · run it in the lesson

Calling APIs from Python Free

spend on active search campaigns

import requests

def active_channel_spend(channel):
    response = requests.get(
        "https://api.gradient.test/v1/campaigns",
        params={"status": "active", "channel": channel},
        timeout=10,
    )
    response.raise_for_status()
    total = 0
    for campaign in response.json()["data"]:
        total += campaign["spend"]
    return total

try:
    print("Search spend:", active_channel_spend("search"))
    print("Email spend:", active_channel_spend("email"))
except requests.exceptions.RequestException as error:
    print("Request failed:", error)

From Calling APIs from Python · run it in the lesson

The requests library

import requests

response = requests.get("https://api.gradient.test/v1/campaigns")

print(response.status_code)
print(response.headers["content-type"])
data = response.json()
print(data["count"], "campaigns")

From Calling APIs from Python · run it in the lesson

Query parameters with params=

import requests

response = requests.get(
    "https://api.gradient.test/v1/campaigns",
    params={"status": "active", "channel": "search"},
)

print(response.url)
for campaign in response.json()["data"]:
    print(campaign["name"], "-", campaign["spend"])

From Calling APIs from Python · run it in the lesson

Sending data with POST

import requests

new_campaign = {"name": "Summer Launch - Social", "budget": 1500, "channel": "social"}
response = requests.post("https://api.gradient.test/v1/campaigns", json=new_campaign)

print(response.status_code)
created = response.json()
print(created["id"], created["status"])

From Calling APIs from Python · run it in the lesson

When things go wrong

import requests

try:
    response = requests.get("https://api.gradient.test/v1/campaigns/cmp_999", timeout=10)
    response.raise_for_status()
    print(response.json()["name"])
except requests.exceptions.HTTPError:
    print("The API refused:", response.status_code, response.json()["error"]["message"])
except requests.exceptions.RequestException as error:
    print("Couldn't complete the request:", error)

From Calling APIs from Python · run it in the lesson

API keys and secrets Free

a script that never contains the key

import os
import requests

os.environ["GRADIENT_API_KEY"] = "gk_demo_7Hq2_not_a_real_secret"   # stands in for your .env file

def get_report():
    key = os.environ.get("GRADIENT_API_KEY")
    if key is None:
        raise RuntimeError("GRADIENT_API_KEY isn't set. Copy .env.example to .env and fill it in.")
    response = requests.get(
        "https://api.gradient.test/v1/report",
        headers={"X-Api-Key": key},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

report = get_report()
print("Status: OK")
print("Report fields:", sorted(report))

From API keys and secrets · run it in the lesson

Sending a key

import requests

url = "https://api.gradient.test/v1/report"

no_key = requests.get(url)
print(no_key.status_code, no_key.json()["error"]["message"])

wrong_key = requests.get(url, headers={"X-Api-Key": "guess-123"})
print(wrong_key.status_code, wrong_key.json()["error"]["message"])

right_key = requests.get(url, headers={"X-Api-Key": "gk_demo_7Hq2_not_a_real_secret"})
print(right_key.status_code, right_key.json())

From API keys and secrets · run it in the lesson

Environment variables

import os

os.environ["GRADIENT_API_KEY"] = "gk_demo_7Hq2_not_a_real_secret"   # stands in for: export GRADIENT_API_KEY=...

key = os.environ.get("GRADIENT_API_KEY")
print("Found a key:", key is not None)

missing = os.environ.get("SOME_OTHER_KEY")
print(missing)

From API keys and secrets · run it in the lesson

Sending email and text messages Free

a rain check before Saturday's hike

import os
import requests

SMS_KEY = os.environ.get("SMS_KEY", "sk_demo_sms_42_not_a_real_secret")
RAINY = {3, 4, 5}   # drizzle, rain, thunderstorm in the practice API's codes

data = requests.get("https://api.gradient.test/v1/forecast", params={"trailhead": "fern-hollow"}).json()
hike = [h for h in data["hours"] if h["time"].startswith("2025-06-14T") and "08" <= h["time"][11:13] <= "11"]
wet = [h for h in hike if h["code"] in RAINY or h["precip_chance"] >= 60]
print(len(hike), "hike hours,", len(wet), "look wet")

if wet:
    first = wet[0]["time"][11:16]
    text = f"Rain check: {data['name']} forecast shows {data['codes'][str(wet[0]['code'])]} from {first}. Move Saturday's hike?"
    reply = requests.post("https://api.gradient.test/v1/sms", headers={"Authorization": f"Bearer {SMS_KEY}"},
                          json={"to": "+15035550188", "body": text})
    print(reply.status_code, len(text), "characters")

From Sending email and text messages · run it in the lesson

An email is a message object

from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "Trail Club <club@shop.example.com>"
msg["To"] = "ada@example.com"
msg["Subject"] = "Your order has shipped"
msg.set_content("Hi Ada,\n\nYour Ridgeline tent is on its way. It should arrive by Friday.\n\nThe shop team")

print(msg["Subject"])
print(msg.as_string()[:300])

From Sending email and text messages · run it in the lesson

Web pages: HTML and CSS Free

Escaping: text that looks like HTML

import html

name = 'Tent <XL> & "Pro"'
print(html.escape(name))
print(f"<td>{html.escape(name)}</td>")
print(html.unescape("Trail &amp; Tor&#39;s &quot;best&quot; &lt;3"))

From Web pages: HTML and CSS · run it in the lesson

Scraping web pages Free

structured data beats the layout

import json
import requests
from bs4 import BeautifulSoup

page = requests.get("https://api.gradient.test/shop/products/TE-101").text
soup = BeautifulSoup(page, "html.parser")

block = soup.find("script", type="application/ld+json")
data = json.loads(block.string)
print(data["@type"], "|", data["name"])
print("price:", data["offers"]["price"], data["offers"]["priceCurrency"])
print("rating:", data["aggregateRating"]["ratingValue"], "from", data["aggregateRating"]["reviewCount"], "reviews")

shown = soup.select_one(".product .now").get_text()
print("the page shows", shown, "and the data says", data["offers"]["price"])

From Scraping web pages · run it in the lesson

Fetch and parse

import requests
from bs4 import BeautifulSoup

reply = requests.get("https://api.gradient.test/shop/deals", params={"page": 1})
print(reply.status_code, reply.headers["content-type"])
soup = BeautifulSoup(reply.text, "html.parser")

print(soup.title.get_text())
cards = soup.select("article.deal")
print(len(cards), "cards")
first = cards[0]
print(first.prettify()[:420])

From Scraping web pages · run it in the lesson

Following pages, politely

import time
from urllib import robotparser

robots = robotparser.RobotFileParser("https://api.gradient.test/robots.txt")
robots.read()
for path in ["/shop/deals?page=2", "/shop/products/TE-101", "/shop/cart?add=TE-101", "/shop/account/"]:
    print(path, robots.can_fetch("*", "https://api.gradient.test" + path))
print("wait between requests:", robots.crawl_delay("*"), "second(s)")

From Scraping web pages · run it in the lesson

Your first LLM call Free

a reusable ask() function

import requests

def ask(question, system="You write short email subject lines for an outdoor clothing brand.", max_tokens=1000):
    response = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"},
        json={
            "model": "claude-opus-5-5",
            "max_tokens": max_tokens,
            "system": system,
            "messages": [{"role": "user", "content": question}],
        },
        timeout=60,
    )
    response.raise_for_status()
    data = response.json()
    if data["stop_reason"] == "max_tokens":
        raise ValueError("The reply was cut off. Raise max_tokens.")
    text = "".join(block["text"] for block in data["content"] if block["type"] == "text")
    return text, data["usage"]

for product in ["rain jackets", "trail shoes"]:
    text, usage = ask(f"Write one subject line for our {product} sale.")
    print(product, "->", text[:80])
    print("  tokens in/out:", usage["input_tokens"], usage["output_tokens"])

From Your first LLM call · run it in the lesson

Capstone: ad-copy reviewer Free

Step 1: load the data

import csv

with open("subjects.csv", "w") as f:
    f.write("""subject,audience
Last chance: spring sale ends tonight,customers
Your March report is ready,partners
""")

with open("subjects.csv") as f:
    rows = list(csv.DictReader(f))

print(len(rows))
print(rows[0])
print(rows[1]["subject"])

From Capstone: ad-copy reviewer · run it in the lesson

Step 2: rules as functions that return problems

SPAMMY = ["free", "winner", "$$$"]

def check_subject(subject):
    problems = [f"spammy word: {word}" for word in SPAMMY if word in subject.lower()]
    if len(subject) > 50:
        problems.append(f"too long ({len(subject)} characters)")
    return problems

print(check_subject("Your March report is ready"))
print(check_subject("FREE gift for our WINNER"))
print(len(check_subject("FREE gift for our WINNER")))

From Capstone: ad-copy reviewer · run it in the lesson

Step 3: a report other tools can read

import json

results = [
    {"subject": "Your March report is ready", "problems": [], "ok": True},
    {"subject": "FREE gift for our WINNER", "problems": ["spammy word: free", "spammy word: winner"], "ok": False},
]

with open("subject_report.json", "w") as f:
    json.dump(results, f, indent=2)

with open("subject_report.json") as f:
    print(f.read())

From Capstone: ad-copy reviewer · run it in the lesson

Step 4: one model call, used carefully

results = [
    {"subject": "Report ready", "problems": []},
    {"subject": "FREE gift for our WINNER", "problems": ["free", "winner"]},
    {"subject": "Free shipping", "problems": ["free"]},
]

worst = results[0]
for r in results:
    if len(r["problems"]) > len(worst["problems"]):
        worst = r
print(worst["subject"])

From Capstone: ad-copy reviewer · run it in the lesson

What an LLM actually does Free

The top choice doesn't always win

import random

options = ["Sunday", "soon", "tomorrow", "March"]
weights = [0.31, 0.22, 0.18, 0.09]

for attempt in range(5):
    pick = random.choices(options, weights=weights)[0]
    print("Our spring sale ends", pick)

From What an LLM actually does · run it in the lesson

Reading math notation as code Free

Hats, bars, stars and Greek letters

y = [3, 5, 2]
y_hat = [2.5, 5, 3]          # ŷ: the model's predictions
y_bar = sum(y) / len(y)      # ȳ: the mean of y, written with a bar
print(y_bar)
print([y[i] - y_hat[i] for i in range(len(y))])   # yᵢ − ŷᵢ, each error

From Reading math notation as code · run it in the lesson

Vectors Free

three customers as points

import numpy as np

ana = np.array([2, 40.0])
ben = np.array([3, 45.0])
cai = np.array([20, 15.0])

print("ana to ben:", round(float(np.linalg.norm(ana - ben)), 1))
print("ana to cai:", round(float(np.linalg.norm(ana - cai)), 1))
print("ben to cai:", round(float(np.linalg.norm(ben - cai)), 1))

From Vectors · run it in the lesson

Vectors in Python

import numpy as np

v = np.array([3, 1])
print(v, v.shape, v.ndim)
print("v1 =", v[0], " v2 =", v[1])

customer = np.array([12, 64.50, 9])   # orders, average order value, days since last order
print(customer.shape)

From Vectors · run it in the lesson

Turning records into vectors

import numpy as np
import requests

FEATURES = ["spend", "clicks", "conversions"]
campaigns = requests.get("https://api.gradient.test/v1/campaigns").json()["data"]

vectors = {c["id"]: np.array([c[f] for f in FEATURES], dtype=float) for c in campaigns}
for cid, v in list(vectors.items())[:3]:
    print(cid, v)

From Vectors · run it in the lesson

What machine learning is Free

learning a cutoff by hand

days    = [10, 25, 40, 70, 105, 130]
churned = [0,  0,  0,  1,  0,   1]

for cutoff in [30, 60, 100]:
    right = 0
    for d, c in zip(days, churned):
        guess = 1 if d > cutoff else 0
        if guess == c:
            right += 1
    print(cutoff, right, "of", len(days))

print("always 'stays':", churned.count(0), "of", len(days))

From What machine learning is · run it in the lesson

The artificial neuron Free

one neuron by hand

import math

x = [1, 3]        # opened the last email (1 = yes), days since last visit
w = [2.0, -0.5]   # weights: how much each input matters
b = -1.0          # bias: the starting point

z = sum(wi * xi for wi, xi in zip(w, x)) + b   # weighted sum plus bias
a = 1 / (1 + math.exp(-z))                      # sigmoid activation
print(z, round(a, 3))

From The artificial neuron · run it in the lesson

The same neuron in NumPy

import numpy as np

def neuron(x, w, b):
    z = np.dot(w, x) + b
    return 1 / (1 + np.exp(-z))          # sigmoid activation

# A "will this customer churn?" neuron: inputs are days since last order (in 100s) and orders per year
w = np.array([1.8, -0.6])
b = -1.0
for x in [[0.2, 9], [1.5, 2], [3.0, 1]]:
    print(f"inputs {x}  →  output {neuron(np.array(x), w, b):.3f}")

From The artificial neuron · run it in the lesson

A neuron draws a straight line

w1, w2, b = 1.0, 1.0, -1.5   # a hand-picked neuron
for x1, x2 in [(0, 0), (0, 1), (1, 0), (1, 1)]:
    z = w1 * x1 + w2 * x2 + b
    print((x1, x2), z, "yes" if z > 0 else "no")

From The artificial neuron · run it in the lesson

Language modeling Free

score a tiny model

import math

# chances the model gave each real next character of "tent"
probs = [0.1, 0.5, 0.4, 0.8]
p_text = math.prod(probs)
loss = sum(-math.log(p) for p in probs) / len(probs)
print(round(p_text, 4))
print(round(loss, 3), round(math.exp(loss), 2))

From Language modeling · run it in the lesson

The chance of a whole text

probs = [0.2, 0.5, 0.9]   # P("c"), P("a" | "c"), P("t" | "ca")
p_text = 1
for p in probs:
    p_text *= p
print(round(p_text, 3))

From Language modeling · run it in the lesson

Scoring a model: the loss

import math

probs = [0.5, 0.4, 0.1]   # chance given to each token that really came next
losses = [-math.log(p) for p in probs]
loss = sum(losses) / len(losses)
print([round(x, 3) for x in losses])
print(round(loss, 3), round(math.exp(loss), 2))

From Language modeling · run it in the lesson

Perplexity: the loss as a number of choices

import math

V = 6                      # a fair die: six equally likely faces
loss = -math.log(1 / V)    # every guess gives the right face 1/6
print(round(loss, 3), round(math.exp(loss), 3))

From Language modeling · run it in the lesson

The course text, and two simple baselines

from collections import Counter

train = "banana"
vocab = ["a", "b", "n", "x"]
counts = Counter(train)
probs = {c: (counts[c] + 1) / (len(train) + len(vocab)) for c in vocab}
print(probs)

From Language modeling · run it in the lesson

What a unigram model can't see

import math
import random
from collections import Counter
import requests

text = requests.get("https://api.gradient.test/v1/corpus").json()["text"]
counts, V = Counter(text), len(set(text))
logp = lambda s: sum(math.log((counts[c] + 1) / (len(text) + V)) for c in s)

sentence = "It is light and packable."
scrambled = "".join(random.Random(1).sample(sentence, len(sentence)))
for s in [sentence, scrambled]:
    print(f"{s!r:<30} log probability {logp(s):.2f}")

From Language modeling · run it in the lesson

Base models vs assistants Free

how much gets resent

questions = [10, 12, 8]    # words in each of your messages
replies = [20, 25, 30]     # words in each of the assistant's replies

history = 0                # words in the conversation so far
total = 0                  # words the model has read across all calls
for call, (q, r) in enumerate(zip(questions, replies), start=1):
    history += q           # your new message joins the conversation
    print(f"call {call}: the model reads {history} words")
    total += history
    history += r           # its reply joins the conversation too

print("you typed:", sum(questions), "words")
print("the model read:", total, "words")

From Base models vs assistants · run it in the lesson

How computers run code Free

Count what the interpreter does

import dis

def one_step(x):
    return x + 1

def three_steps(x):
    y = x * 2
    y = y + 1
    return y - 3

for f in (one_step, three_steps):
    count = len(list(dis.get_instructions(f)))
    print(f.__name__, count, "instructions")

From How computers run code · run it in the lesson

Everything is numbers

print(ord("A"), ord("a"), ord("€"))
print(bin(65))               # 65 written in base 2: the bits themselves
data = "café".encode("utf-8")
print(data, len(data))       # é needs two bytes in UTF-8
print(data.decode("utf-8"))  # and back again

From How computers run code · run it in the lesson

Memory: a long row of numbered boxes

a = [4, 9]
b = a              # b points at the SAME list, no copy is made
b.append(12)
print(a)           # a changed too
print(id(a) == id(b))

c = list(a)        # list() builds a new list with the same items
c.append(99)
print(a, c)

From How computers run code · run it in the lesson

The processor: fetch, decode, execute

import dis

def total(price, qty):
    return price * qty + 5

for ins in dis.get_instructions(total):
    print(ins.opname, ins.argrepr)

From How computers run code · run it in the lesson

HTTP from the wire up Free

Build the bytes yourself

import json

body = json.dumps({"name": "Spring", "budget": 500}).encode("utf-8")
head = (
    "POST /v1/campaigns?dry_run=1 HTTP/1.1\r\n"
    "Host: api.gradient.test\r\n"
    "Content-Type: application/json\r\n"
    f"Content-Length: {len(body)}\r\n"
    "\r\n"
)
raw = head.encode("ascii") + body
print(raw)
print(len(body), "body bytes;", len(raw), "bytes in total")

From HTTP from the wire up · run it in the lesson

URLs and query strings

from urllib.parse import urlsplit, urlencode, parse_qs, quote

url = "https://api.gradient.test/v1/products?category=home&q=caf%C3%A9#reviews"
parts = urlsplit(url)
print(parts.scheme, parts.hostname, parts.path, parts.fragment)
print(parse_qs(parts.query))

print(urlencode({"q": "tents & tarps", "max_price": 200}))
print(quote("reports/Q3 final.pdf"))

From HTTP from the wire up · run it in the lesson

JSON-RPC 2.0 in depth Free

Which error code?

import json

METHODS = {"tools/list": {"params": []}, "tools/call": {"params": ["name"]}}

def check(raw):
    try:
        msg = json.loads(raw)
    except ValueError:
        return -32700, "parse error"
    if not isinstance(msg, dict) or msg.get("jsonrpc") != "2.0" or "method" not in msg:
        return -32600, "invalid request"
    if msg["method"] not in METHODS:
        return -32601, "method not found"
    for needed in METHODS[msg["method"]]["params"]:
        if needed not in msg.get("params", {}):
            return -32602, "invalid params"
    return 0, "ok"

for raw in ['{"jsonrpc": ',
            '{"id": 1, "method": "tools/list"}',
            '{"jsonrpc": "2.0", "id": 2, "method": "tools/delete"}',
            '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {}}',
            '{"jsonrpc": "2.0", "id": 4, "method": "tools/list"}']:
    print(check(raw))

From JSON-RPC 2.0 in depth · run it in the lesson

One message in, at most one out

import json

def handle(raw, methods):
    try:
        msg = json.loads(raw)
    except json.JSONDecodeError:
        return {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}}
    if "id" not in msg:
        if msg.get("method") in methods:
            methods[msg["method"]](msg.get("params", {}))
        return None                                  # notifications get no reply
    fn = methods.get(msg.get("method"))
    if fn is None:
        return {"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32601, "message": f"Method not found: {msg.get('method')}"}}
    return {"jsonrpc": "2.0", "id": msg["id"], "result": fn(msg.get("params", {}))}

seen = []
methods = {
    "echo": lambda p: {"you_said": p.get("text")},
    "notifications/log": lambda p: seen.append(p["line"]),
}
for raw in ['{"jsonrpc": "2.0", "id": 1, "method": "echo", "params": {"text": "hi"}}',
            '{"jsonrpc": "2.0", "method": "notifications/log", "params": {"line": "started"}}',
            '{"jsonrpc": "2.0", "id": 2, "method": "missing"}',
            '{"jsonrpc": "2.0", "id": 3, "method": ']:
    print(handle(raw, methods))
print("logged:", seen)

From JSON-RPC 2.0 in depth · run it in the lesson

System design fundamentals Free

A back-of-the-envelope estimate

users = 200_000
requests_each = 25
peak_factor = 3
per_server = 500       # requests a second one server handles
order_size_kb = 2
orders_a_day = 8_000

average = users * requests_each / 86_400
peak = average * peak_factor
servers = int(peak // per_server) + 1 + 1   # round up, plus one spare

print("average requests a second:", round(average))
print("peak requests a second:", round(peak))
print("servers:", servers)
print("order storage a year (GB):", round(orders_a_day * order_size_kb * 365 / 1_000_000, 1))

From System design fundamentals · run it in the lesson

What a cache does to the database

reads = 2000     # reads a second

for hit_rate in [0, 0.5, 0.9, 0.99]:
    to_database = reads * (1 - hit_rate)
    print(f"hit rate {hit_rate:>4.0%}: {to_database:>6.0f} reads a second reach the database")

From System design fundamentals · run it in the lesson

Why modulo sharding hurts

import hashlib

def h(text):
    return int(hashlib.sha256(text.encode()).hexdigest()[:8], 16)

keys = [f"user-{i}" for i in range(1000)]
before = {k: h(k) % 4 for k in keys}
after = {k: h(k) % 5 for k in keys}
moved = sum(before[k] != after[k] for k in keys)
print(f"{moved} of 1000 keys moved")

From System design fundamentals · run it in the lesson

Every lesson has its own recipes

These are from the 55 free lessons. A free account opens all 345 lessons, each ending in a gallery like this, with the previews run live.

Create a free account