Industry adoption: Instagram, Spotify, Dropbox, Netflix
Data science integration: Same language for APIs and ML
The Request-Response Mental Model
# This is how you should think about APIs
def handle_request(request_data):
# 1. Receive data from client
user_id = request_data["user_id"]
# 2. Process the request
user = find_user(user_id)
# 3. Return data to client
return {"id": user.id, "name": user.name}
# Database records (simulated)
users_db = [
{"id": 1, "name": "Alice", "password": "secret123"},
{"id": 2, "name": "Bob", "password": "hunter2"},
]
# Build API response - exclude sensitive data!
def get_users():
return {
"users": [
{"id": u["id"], "name": u["name"]} # No password!
for u in users_db
]
}
List comprehensions transform data efficiently
Never expose sensitive data like passwords
Shape the response to what clients need
Type Mapping: Python to JSON
Python Type
JSON Type
Example
dict
object
{"key": "value"}
list, tuple
array
[1, 2, 3]
str
string
"hello"
int, float
number
42, 3.14
True, False
true, false
true
None
null
null
Warning: datetime, set, and custom objects don't serialize to JSON automatically!
Part 4: Functions as Request Handlers
Building the pattern for Flask routes
The Handler Pattern
def get_user(user_id):
"""Handle a GET request for a single user."""
# Validate input
if not isinstance(user_id, int) or user_id < 1:
return {"error": "Invalid user ID"}, 400
# Find the user
user = find_user_by_id(user_id)
if user is None:
return {"error": "User not found"}, 404
# Return success response
return {"id": user["id"], "name": user["name"]}, 200
Validate input first
Return error responses with appropriate status codes
Return success response with data
CRUD Operations as Functions
# In-memory "database" for now
users = {}
next_id = 1
def create_user(data):
global next_id
user = {"id": next_id, "name": data["name"]}
users[next_id] = user
next_id += 1
return user
def get_user(user_id):
return users.get(user_id)
def update_user(user_id, data):
if user_id in users:
users[user_id].update(data)
return users[user_id]
return None
def delete_user(user_id):
return users.pop(user_id, None)
CRUD Maps to HTTP Methods
Operation
HTTP Method
Function
URL Pattern
Create
POST
create_user(data)
/users
Read (one)
GET
get_user(id)
/users/{id}
Read (all)
GET
list_users()
/users
Update
PUT/PATCH
update_user(id, data)
/users/{id}
Delete
DELETE
delete_user(id)
/users/{id}
Each function becomes a Flask route handler
HTTP methods indicate the operation type
RESTful APIs follow this convention
Function Parameters in APIs
def search_users(name=None, limit=10, offset=0):
"""Search users with optional filters."""
results = list(users.values())
# Filter by name if provided
if name:
results = [u for u in results if name.lower() in u["name"].lower()]
# Apply pagination
total = len(results)
results = results[offset:offset + limit]
return {
"users": results,
"total": total,
"limit": limit,
"offset": offset
}
Default parameters handle optional query strings
Pagination prevents returning too much data
Filtering lets clients request specific data
Error Handling Pattern
def get_user(user_id):
# Validate input type
try:
user_id = int(user_id)
except ValueError:
return {"error": "User ID must be an integer"}, 400
# Check if user exists
user = users.get(user_id)
if user is None:
return {"error": f"User {user_id} not found"}, 404
return user, 200
400: Bad Request - client sent invalid data
404: Not Found - resource doesn't exist
200: OK - successful response
Always return helpful error messages
Part 5: OOP Basics for Models
Classes as structured data containers
Why Classes for Backend Data?
Structure: Define what fields an object has
Validation: Ensure data is correct on creation
Methods: Add behavior to your data
Reusability: Create many instances from one template
Preview: SQLAlchemy models are classes that map to database tables.
A Simple User Class
class User:
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
return {
"id": self.id,
"name": self.name,
"email": self.email
}
# Creating a user
user = User(1, "Alice", "alice@example.com")
print(user.to_dict()) # {"id": 1, "name": "Alice", "email": "alice@example.com"}
__init__ is the constructor - called when creating an instance
self refers to the instance being created
to_dict() converts the object for JSON serialization
Adding Validation
class User:
def __init__(self, id, name, email):
if not name or len(name) < 2:
raise ValueError("Name must be at least 2 characters")
if "@" not in email:
raise ValueError("Invalid email address")
self.id = id
self.name = name
self.email = email
# This works
user = User(1, "Alice", "alice@example.com")
# This raises an error
user = User(2, "A", "not-an-email") # ValueError!
Validation in __init__ ensures data integrity
Invalid data fails fast with clear error messages
Class Methods for Business Logic
class User:
def __init__(self, id, name, email, password_hash=None):
self.id = id
self.name = name
self.email = email
self.password_hash = password_hash
def to_dict(self):
# Note: Never include password_hash!
return {"id": self.id, "name": self.name, "email": self.email}
def to_public_dict(self):
# Even more limited for public APIs
return {"id": self.id, "name": self.name}
def check_password(self, password):
# Verify password (simplified example)
return hash(password) == self.password_hash
Different methods for different serialization needs
Business logic lives in the model class
Using Classes with Our Handler Pattern
class User:
# ... (as before)
pass
# Our "database"
users = {}
def create_user(data):
try:
user = User(
id=len(users) + 1,
name=data["name"],
email=data["email"]
)
users[user.id] = user
return user.to_dict(), 201
except ValueError as e:
return {"error": str(e)}, 400
except KeyError as e:
return {"error": f"Missing required field: {e}"}, 400
Classes provide structure, functions provide API logic
Validation errors become 400 responses
201 status code means "Created"
Preview: SQLAlchemy Models
# Week 7: This is what database models look like
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def to_dict(self):
return {"id": self.id, "name": self.name, "email": self.email}
Same to_dict() pattern you learned today
SQLAlchemy adds database persistence
The class structure is nearly identical
Live Demo
From Python Dict to JSON API Response
Demo: What We'll Build
Start with a simple Python dictionary
Convert it to JSON with pretty printing
Write a function that returns user data
Create a User class with to_dict()
See how it all connects to API responses
Watch along: I'll type everything live.
You'll practice in the in-class exploration.
Summary
Key takeaways from today
What You Learned Today
Python for backends: Clean, productive, industry-standard
Dictionaries = JSON: Python data structures map directly to API responses
json module:dumps() and loads() for conversion
Functions as handlers: Input, process, output pattern
CRUD operations: Create, Read, Update, Delete
Classes for models: Structure with to_dict() for serialization
Looking Ahead
Week
Topic
Building On
3
REST APIs with Flask
Functions become routes
4
Docker
Containerize your Flask app
6
Validation
Marshmallow schemas
7
Databases
Classes become SQLAlchemy models
Lab 1 (Friday): Python practice exercises.
Prep for next week: O'Reilly Chapter 3 - Your First REST API
In-Class Exploration
Student Records API (Python Only)
Project Overview
Build Python functions for a student records system