What’s one thing you learned? What’s still confusing?
Mini-Project: Tip Calculator
Build a tip + tax + split calculator with default parameters and tuple returns — your first reusable mini-library.
Lists, Tuples & Sets
Python's core sequences — lists, tuples, sets, frozensets — with time complexity of every operation.
List Comprehensions: Python's Superpower
Write concise, fast list/dict/set transformations in one readable line.
Interactive Labs for This Track
Loop Visualizer
You're a factory robot repeating the same task on an assembly line — watch how loops automate repetitive work
List Slicing
You have a playlist of 50 songs — grab just tracks 10 through 20 with a single slice expression
Sorting Algorithms
You're organizing a library of 10,000 books — which sorting method is fastest?
Ask questions, share insights
client.messages.create(...). Master functions — parameters, returns, scope, closures — and you've mastered 70% of what makes a Python engineer effective.A function is a named, reusable block of code that takes input, does something, and (optionally) returns output.
print(), len(), type(), range(), int(), input(). Now you will learn to create your own.# Defining a function
def greet(name):
"""Say hello to someone."""
return f"Hello, {name}! Welcome to Python."
# Calling (using) the function
message = greet("Alex")
print(message) # Hello, Alex! Welcome to Python.
print(greet("AI")) # Hello, AI! Welcome to Python.Let us break down the syntax:
def -- keyword that starts a function definitiongreet -- the function's name (use snake_case)(name) -- the parameter (input variable)""" ... """ -- docstring (documentation). Describes what the function doesreturn -- sends a value back to the caller. Without it, the function returns Nonedef calculate_bmi(weight_kg, height_m):
"""Calculate Body Mass Index."""
bmi = weight_kg / (height_m ** 2)
return round(bmi, 1)
# Positional arguments (order matters)
result = calculate_bmi(70, 1.75)
print(f"BMI: {result}") # BMI: 22.9
# Keyword arguments (order does not matter)
result = calculate_bmi(height_m=1.75, weight_kg=70)
print(f"BMI: {result}") # BMI: 22.9Given `def calculate_bmi(weight_kg, height_m):`, which call computes the BMI for someone who weighs 70kg and is 1.75m tall?
Functions can return multiple values using tuples:
def analyze_scores(scores):
"""Return statistics about a list of scores."""
avg = sum(scores) / len(scores)
highest = max(scores)
lowest = min(scores)
return avg, highest, lowest # returns a tuple
# Unpack the results
average, high, low = analyze_scores([85, 92, 78, 95, 88])
print(f"Average: {average}, High: {high}, Low: {low}")
# Average: 87.6, High: 95, Low: 78What does a function return if there is no return statement?
return statement (or just return with no value), it returns None -- Python's way of saying "no value." This is important to know because accidentally forgetting return is a very common bug.def say_hello(name):
print(f"Hello, {name}!") # this PRINTS but does not RETURN
result = say_hello("Alex")
print(result) # None -- because there is no return statement!greet, bind name and the default greeting, and pop the frame on return — every variable change, every print, live.Edit the code, then click Trace it. Python actually runs in your browser — every line, every variable, every print.
Click Trace it to capture the execution trace. The scrubber below will let you step through every variable change line-by-line.
You can give parameters default values, making them optional when calling the function:
def train_model(data, epochs=10, learning_rate=0.001, verbose=True):
"""Simulate model training with configurable parameters."""
if verbose:
print(f"Training on {len(data)} samples")
print(f"Epochs: {epochs}, LR: {learning_rate}")
# Simulate training
loss = 100.0
for epoch in range(epochs):
loss *= (1 - learning_rate * 10)
if verbose and epoch % 3 == 0:
print(f" Epoch {epoch}: loss = {loss:.4f}")
return loss
# Use all defaults
train_model([1, 2, 3, 4, 5])
# Override some defaults
train_model([1, 2, 3, 4, 5], epochs=20, learning_rate=0.01)
# Override just one
train_model([1, 2, 3, 4, 5], verbose=False)# WRONG
# def bad_func(x=10, y): # SyntaxError
# RIGHT
def good_func(y, x=10):
return x + ymodel.fit(X, y, epochs=10, batch_size=32, verbose=1), those keyword arguments with defaults are exactly this pattern.What does the second call print? def add_item(item, items=[]): items.append(item) return items print(add_item("a")) print(add_item("b"))
items=[] exactly ONCE — at function-definition time — and reuses that SAME list on every call that omits the argument. So the first call appends "a" to the shared list, and the second call appends "b" to the SAME list (which still has "a" in it). This is Python's most infamous gotcha, and it has caused real production bugs. The fix: use items=None and create a fresh list inside: if items is None: items = [].Sometimes you want a function to accept any number of arguments.
*args -- Variable Positional Argumentsdef average(*args):
"""Calculate the average of any number of values."""
if not args:
return 0
return sum(args) / len(args)
print(average(10, 20)) # 15.0
print(average(1, 2, 3, 4, 5)) # 3.0
print(average(100)) # 100.0*args collects all positional arguments into a tuple. The name args is a convention -- you could call it *numbers or *values.**kwargs -- Variable Keyword Argumentsdef create_model(**kwargs):
"""Create a model configuration from keyword arguments."""
config = {
"type": "neural_network", # default
"layers": 3, # default
"learning_rate": 0.001, # default
}
config.update(kwargs) # override defaults with provided values
return config
model1 = create_model()
print(model1)
# {'type': 'neural_network', 'layers': 3, 'learning_rate': 0.001}
model2 = create_model(layers=12, dropout=0.1, name="GPT-mini")
print(model2)
# {'type': 'neural_network', 'layers': 12, 'learning_rate': 0.001, 'dropout': 0.1, 'name': 'GPT-mini'}**kwargs collects all keyword arguments into a dictionary. This is extremely common in AI libraries where models have dozens of optional parameters.def flexible_function(required, *args, default="hello", **kwargs):
print(f"Required: {required}")
print(f"Extra positional: {args}")
print(f"Default: {default}")
print(f"Extra keyword: {kwargs}")
flexible_function("yes", 1, 2, 3, default="world", color="blue", size=10)
# Required: yes
# Extra positional: (1, 2, 3)
# Default: world
# Extra keyword: {'color': 'blue', 'size': 10}*args, keyword-only parameters (with defaults), **kwargs.# Regular function
def double(x):
return x * 2
# Same thing as a lambda
double = lambda x: x * 2
print(double(5)) # 10Lambdas are most useful when passed to other functions:
# Sort a list of tuples by the second element
students = [("Alice", 92), ("Bob", 78), ("Charlie", 95), ("Diana", 88)]
# Sort by grade (second element of each tuple)
students.sort(key=lambda student: student[1])
print(students)
# [('Bob', 78), ('Diana', 88), ('Alice', 92), ('Charlie', 95)]
# Sort by grade descending
students.sort(key=lambda student: student[1], reverse=True)
print(students)
# [('Charlie', 95), ('Alice', 92), ('Diana', 88), ('Bob', 78)]
# Filter with a lambda
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4, 6, 8, 10]
# Map with a lambda
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]def function instead.# Global variable -- accessible everywhere
model_name = "GPT-4"
def train():
# Local variable -- only exists inside this function
epochs = 10
print(f"Training {model_name} for {epochs} epochs") # can READ global
train()
# print(epochs) # NameError: 'epochs' is not defined (it is local to train)
print(model_name) # "GPT-4" -- global variables persistx = 100 # global
def demo():
x = 999 # local x -- does NOT change the global x
print(f"Inside function: x = {x}") # 999
demo()
print(f"Outside function: x = {x}") # 100 -- unchanged!count. Without scope, they would overwrite each other's values. With scope, each function has its own private count that cannot interfere.What gets printed? count = 10 def bump(): count = count + 1 print(count) bump()
count = ... anywhere in a function body, it marks count as a LOCAL variable for the entire function — even on lines BEFORE the assignment. So count + 1 tries to read a local count that hasn't been assigned yet, and crashes. The global count = 10 is invisible inside bump() because of the assignment. To actually modify the global, you need global count at the top of the function (or, more commonly, just return the new value: return count + 1).Scope prevents accidents like this, but the error message can confuse beginners — it says "local variable referenced before assignment," but the cause is the assignment LATER in the function.
def count_vowels(text):
count = 0 # local to this function
for char in text.lower():
if char in "aeiou":
count += 1
return count
def count_words(text):
count = len(text.split()) # different variable, local to THIS function
return count
# These do not interfere with each other
print(count_vowels("Hello World")) # 3
print(count_words("Hello World")) # 2HitUnboundLocalErrororTypeError: missing argument? Reading a name that's later assigned inside the same function triggersUnboundLocalError, and forgetting a positional arg triggersTypeError. See the error decoder for both.
def clean_text(text):
"""Lowercase, strip whitespace, remove punctuation."""
import string
text = text.lower().strip()
text = text.translate(str.maketrans("", "", string.punctuation))
return text
def tokenize(text):
"""Split text into individual words."""
return text.split()
def remove_stop_words(tokens, stop_words=None):
"""Remove common words that do not carry meaning."""
if stop_words is None:
stop_words = {"the", "a", "an", "is", "are", "was", "in", "on", "at", "to", "of"}
return [word for word in tokens if word not in stop_words]
def preprocess(text):
"""Full preprocessing pipeline."""
cleaned = clean_text(text)
tokens = tokenize(cleaned)
filtered = remove_stop_words(tokens)
return filtered
# Use it
result = preprocess("The Cat is sitting ON the Mat!")
print(result) # ['cat', 'sitting', 'mat']def accuracy(predictions, actual):
"""Calculate classification accuracy."""
correct = sum(p == a for p, a in zip(predictions, actual))
return correct / len(actual)
def precision(predictions, actual, positive_label="spam"):
"""Of all items we predicted as positive, how many were actually positive?"""
true_positives = sum(p == a == positive_label for p, a in zip(predictions, actual))
predicted_positive = sum(p == positive_label for p in predictions)
return true_positives / predicted_positive if predicted_positive > 0 else 0.0
def evaluate_model(predictions, actual):
"""Run full evaluation suite."""
acc = accuracy(predictions, actual)
prec = precision(predictions, actual)
return {
"accuracy": round(acc, 4),
"precision": round(prec, 4),
"total_samples": len(actual),
}
# Test it
preds = ["spam", "ham", "spam", "spam", "ham", "spam", "ham", "ham"]
truth = ["spam", "ham", "ham", "spam", "ham", "spam", "spam", "ham"]
results = evaluate_model(preds, truth)
for metric, value in results.items():
print(f"{metric}: {value}")Tests · Write each function, then test it with print() to verify it works!
Functions can call other functions, building up complexity from simple pieces:
def is_positive(text):
"""Check if text contains positive words."""
positive_words = {"good", "great", "awesome", "love", "amazing", "excellent"}
words = set(text.lower().split())
return len(words & positive_words) > 0
def is_negative(text):
"""Check if text contains negative words."""
negative_words = {"bad", "terrible", "awful", "hate", "horrible", "worst"}
words = set(text.lower().split())
return len(words & negative_words) > 0
def classify_sentiment(text):
"""Classify text as positive, negative, or neutral."""
pos = is_positive(text)
neg = is_negative(text)
if pos and not neg:
return "positive"
elif neg and not pos:
return "negative"
elif pos and neg:
return "mixed"
else:
return "neutral"
# Test the pipeline
reviews = [
"This movie was great and amazing!",
"Terrible experience, the worst ever.",
"It was okay, nothing special.",
"I love the great food but hate the terrible service.",
]
for review in reviews:
sentiment = classify_sentiment(review)
print(f"[{sentiment:>8}] {review}")This pattern -- small, focused functions composed into larger workflows -- is how real AI systems are built. Each function does one thing well, and you chain them together.
Visualize Python's LEGB scope resolution, call stack frames, and closure variable capture:
x, it searches four concentric scopes in a fixed order: Local → Enclosing → Global → Built-in. The first match wins; if none match, you get NameError. The visualizer below makes the search visible — a ball expands outward through the rings until it finds the binding, then snaps back to the call site with the value. Six presets cover every shape this lookup can take, including the nonlocal / global write-target rules.This function should return a fresh list with the new item each time it's called. Instead, items keep accumulating across calls. Fix the function.
['apple'] ['banana'] ['cherry']
Write a function `calculate_total(bill, tip_pct=15)` that returns the total amount to pay (bill + tip). The tip percentage should default to 15%. Then call it twice — once with default tip, once with a custom tip — and print both totals.
Total on $50 (default 15%): $57.50
Total on $80 (20% tip): $96.00def calculate_total(bill, tip_pct=15):
# TODO: compute tip and return bill + tip
pass
# Call once with default tip, once with custom tip
total1 = calculate_total(50)
total2 = calculate_total(80, 20)
print(f"Total on $50 (default 15%): ${total1:.2f}")
print(f"Total on $80 (20% tip): ${total2:.2f}")def, pass inputs as parameters, send outputs back with return. A function without return gives back Nonedef train(epochs=10) lets callers skip the epochs argument. This pattern is everywhere in AI libraries*args and **kwargs accept variable arguments -- *args collects extra positional arguments into a tuple, **kwargs collects extra keyword arguments into a dictionarylambda x: x * 2 is perfect for sorting keys and filter/map operations. Use def for anything more complexWhat does this function return? def add(a, b): c = a + b