What’s one thing you learned? What’s still confusing?
Classes & Object-Oriented Programming
Create classes with __init__, methods, @property, __slots__, and inheritance basics.
Mini-Project: Bank Account Class
Build a BankAccount class with deposit, withdraw, history, and transfers.
Advanced OOP, Part 1: Inheritance, MRO & ABCs
Multiple inheritance, MRO with C3 linearization, cooperative super(), the mixin pattern, and Abstract Base Classes.
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
mypy --strict (or pyright) on their Python codebases. ruff has replaced flake8/black/isort as the standard linter — it's written in Rust and runs 100× faster. By the end of this lesson, your toolchain matches theirs.What will mypy report about this function?
Type hints tell Python (and developers) what types a function expects and returns. They do not enforce anything at runtime -- Python remains dynamically typed. But editors and tools use them to catch errors.
# Without type hints -- works, but unclear
def add(a, b):
return a + b
# With type hints -- immediately clear what is expected
def add(a: int, b: int) -> int:
return a + b
# Variable type hints
name: str = "Alice"
age: int = 25
gpa: float = 3.8
is_student: bool = Truefrom typing import List, Dict, Tuple, Optional, Union
# Lists of a specific type
def average(numbers: List[float]) -> float:
"""Calculate the average of a list of numbers."""
return sum(numbers) / len(numbers)
# Dictionaries with typed keys and values
def get_student_grades(roster: Dict[str, List[int]]) -> Dict[str, float]:
"""Convert a roster of grade lists to averages."""
return {name: sum(grades) / len(grades) for name, grades in roster.items()}
# Tuples with specific element types
def get_coordinates() -> Tuple[float, float]:
"""Return a latitude/longitude pair."""
return (37.7749, -122.4194)
# Optional: can be the type or None
def find_user(user_id: int) -> Optional[str]:
"""Find a user by ID. Returns None if not found."""
users = {1: "Alice", 2: "Bob", 3: "Charlie"}
return users.get(user_id)
# Union: can be one of several types
def normalize(value: Union[int, float, str]) -> float:
"""Convert various inputs to a float."""
if isinstance(value, str):
return float(value)
return float(value)What is the type of x after the if-check below?
# Python 3.10+ uses | instead of Union and built-in types instead of typing imports
def normalize(value: int | float | str) -> float:
"""Convert various inputs to a float."""
if isinstance(value, str):
return float(value)
return float(value)
def find_user(user_id: int) -> str | None:
"""Find a user by ID. Returns None if not found."""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
# Built-in generics (no import needed)
def average(numbers: list[float]) -> float:
return sum(numbers) / len(numbers)
def get_grades(roster: dict[str, list[int]]) -> dict[str, float]:
return {name: sum(g) / len(g) for name, g in roster.items()}assert statements. An assert checks that a condition is True -- if it is False, it raises an AssertionError.# The function we want to test
def celsius_to_fahrenheit(celsius: float) -> float:
"""Convert Celsius temperature to Fahrenheit."""
return celsius * 9 / 5 + 32
# Tests using assert
assert celsius_to_fahrenheit(0) == 32, "Freezing point should be 32F"
assert celsius_to_fahrenheit(100) == 212, "Boiling point should be 212F"
assert celsius_to_fahrenheit(-40) == -40, "-40 is the same in both scales"
print("All temperature tests passed!")class Calculator:
"""A simple calculator with history."""
def __init__(self) -> None:
# Built-in generic syntax (Python 3.9+). No import needed.
self.history: list[str] = []
def add(self, a: float, b: float) -> float:
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def subtract(self, a: float, b: float) -> float:
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result
def multiply(self, a: float, b: float) -> float:
result = a * b
self.history.append(f"{a} * {b} = {result}")
return result
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
result = a / b
self.history.append(f"{a} / {b} = {result}")
return result
# Test suite
def test_calculator() -> None:
"""Run all calculator tests."""
calc = Calculator()
# Test basic operations
assert calc.add(2, 3) == 5
assert calc.subtract(10, 4) == 6
assert calc.multiply(3, 7) == 21
assert calc.divide(15, 3) == 5.0
# Test edge cases
assert calc.add(0, 0) == 0
assert calc.add(-1, 1) == 0
assert calc.multiply(0, 1000) == 0
# Test division by zero raises an error
try:
calc.divide(10, 0)
assert False, "Should have raised ValueError"
except ValueError as e:
assert str(e) == "Cannot divide by zero"
# Test history tracking
assert len(calc.history) == 7 # 7 operations above (before the error)
print("All calculator tests passed!")
test_calculator()def test_edge_cases() -> None:
"""Demonstrate common testing patterns."""
# Pattern 1: Test expected output
assert abs(average([1.0, 2.0, 3.0]) - 2.0) < 1e-10, "Average of [1,2,3] should be 2"
# Pattern 2: Test with floating point (use approximate equality)
result = 0.1 + 0.2
assert abs(result - 0.3) < 1e-10, f"Expected ~0.3, got {result}"
# Pattern 3: Test that exceptions are raised
try:
int("not_a_number")
assert False, "Should have raised ValueError"
except ValueError:
pass # expected -- test passes
# Pattern 4: Test type of result
assert isinstance(find_user(1), str), "Should return a string"
assert find_user(999) is None, "Should return None for missing user"
# Pattern 5: Test collections
result_list = sorted([3, 1, 2])
assert result_list == [1, 2, 3], "Should be sorted ascending"
assert len(result_list) == 3, "Should have 3 elements"
print("All edge case tests passed!")
test_edge_cases()# Variables and functions: snake_case
student_name = "Alice"
learning_rate = 0.001
def calculate_loss(predictions: List[float], targets: List[float]) -> float:
"""Calculate mean squared error loss."""
n = len(predictions)
return sum((p - t) ** 2 for p, t in zip(predictions, targets)) / n
# Classes: PascalCase
class NeuralNetwork:
pass
class DataLoader:
pass
# Constants: UPPER_SNAKE_CASE
MAX_EPOCHS = 100
LEARNING_RATE = 0.001
BATCH_SIZE = 32
# Private attributes/methods: leading underscore
class Model:
def __init__(self) -> None:
self._weights: List[float] = [] # internal -- do not access directly
def _validate_input(self, data: List[float]) -> bool:
"""Internal validation -- not part of the public API."""
return len(data) > 0def train_model(
data: List[List[float]],
labels: List[int],
learning_rate: float = 0.01,
epochs: int = 100,
) -> Dict[str, List[float]]:
"""Train a simple linear model on the given data.
Args:
data: Training samples, each a list of features.
labels: Integer labels for each sample (0 or 1).
learning_rate: Step size for gradient updates. Default 0.01.
epochs: Number of training iterations. Default 100.
Returns:
A dictionary with 'losses' (list of loss per epoch)
and 'accuracy' (list of accuracy per epoch).
Raises:
ValueError: If data and labels have different lengths.
Example:
>>> result = train_model([[1, 2], [3, 4]], [0, 1])
>>> len(result['losses'])
100
"""
if len(data) != len(labels):
raise ValueError(f"data has {len(data)} samples but labels has {len(labels)}")
losses: List[float] = []
accuracies: List[float] = []
# ... training logic would go here ...
return {"losses": losses, "accuracy": accuracies}# Python linters check your code automatically:
# 1. flake8 -- checks PEP 8 style (line length, whitespace, imports)
# pip install flake8
# flake8 my_script.py
# 2. mypy -- checks type hints for correctness
# pip install mypy
# mypy my_script.py
# 3. black -- auto-formats code to a consistent style
# pip install black
# black my_script.py
# 4. ruff -- ultra-fast linter (replaces flake8 + isort + more)
# pip install ruff
# ruff check my_script.py
# Example: mypy would catch this error without running the code:
# def add(a: int, b: int) -> int:
# return a + b
#
# result = add("hello", "world") # mypy error: str is not int# Python 3.10+: use built-in generics (list, dict) and `X | None` instead of
# Optional[X]. No typing imports needed.
class GradeBook:
"""A grade book that tracks student scores with full type safety."""
def __init__(self) -> None:
self._grades: dict[str, list[float]] = {}
def add_grade(self, student: str, grade: float) -> None:
"""Add a grade for a student."""
if grade < 0 or grade > 100:
raise ValueError(f"Grade must be 0-100, got {grade}")
if student not in self._grades:
self._grades[student] = []
self._grades[student].append(grade)
def get_average(self, student: str) -> float | None:
"""Get a student's average grade, or None if no grades exist."""
grades = self._grades.get(student)
if not grades:
return None
return sum(grades) / len(grades)
def get_top_student(self) -> str | None:
"""Get the student with the highest average."""
if not self._grades:
return None
return max(self._grades, key=lambda s: sum(self._grades[s]) / len(self._grades[s]))
def get_all_averages(self) -> dict[str, float]:
"""Get averages for all students."""
return {
student: sum(grades) / len(grades)
for student, grades in self._grades.items()
}
def test_grade_book() -> None:
"""Comprehensive tests for GradeBook."""
gb = GradeBook()
# Test adding grades
gb.add_grade("Alice", 95)
gb.add_grade("Alice", 87)
gb.add_grade("Bob", 78)
gb.add_grade("Bob", 82)
gb.add_grade("Charlie", 92)
# Test averages
assert gb.get_average("Alice") == 91.0
assert gb.get_average("Bob") == 80.0
assert gb.get_average("Charlie") == 92.0
assert gb.get_average("Unknown") is None
# Test top student
assert gb.get_top_student() == "Charlie"
# Test all averages
averages = gb.get_all_averages()
assert len(averages) == 3
assert averages["Alice"] == 91.0
# Test invalid grades
try:
gb.add_grade("Alice", -5)
assert False, "Should reject negative grade"
except ValueError:
pass
try:
gb.add_grade("Alice", 105)
assert False, "Should reject grade > 100"
except ValueError:
pass
print("All GradeBook tests passed!")
test_grade_book()Tests · Add type hints, write test assertions, and build a GradeBook!
Interactive Lab
See how Python type hints annotate functions and catch errors before runtime
int, str, float, bool for primitives, and List, Dict, Optional, Union from typing for complex types. In Python 3.10+ use list[int] and str | None directlyassert condition, "error message" checks that a condition is True. Group related tests in functions like test_calculator()What does -> int mean in a function signature?
snake_case for variables and functions, PascalCase for classes, UPPER_SNAKE_CASE for constants, leading _underscore for private attributes