Table of Contents
- Flask Assignments — Complete Tutorial Guide
- Prerequisites
- Assignment 01: Basic To-Do API
- What You Will Learn
- Key Concepts
- Setup
- Running the Server
- Project Files
- Testing Step by Step
- Common Issues
- Assignment 02: Personal Blog
- Assignment 03: User Profile & Uploads
- Assignment 04: Task Manager with Background Tasks
- Assignment 05: Weather Dashboard
- Assignment 06: Real-time Chat
- What You Will Learn
- Key Concepts
- Setup
- Running the Server
- Project Files
- Testing Step by Step
- Common Issues
- Assignment 07: Form Handling & Jinja2 Templates
- Assignment 08: Webhook Receiver
- Assignment 09: Inventory Management
- Assignment 10: Secure Document Vault
Flask Assignments — Complete Tutorial Guide
This guide walks through all ten assignments in the flask-assignments/ directory. Flask is a micro-framework: it provides routing and a request context, but deliberately leaves decisions about databases, validation, and authentication to the developer. This makes it an excellent tool for learning because every component is explicit.
Prerequisites
Python Version
Flask 3.x requires Python 3.8 or later. Verify:
python --version
Virtual Environments
Use a separate virtual environment for each assignment to avoid dependency conflicts between projects.
python -m venv venv
source venv/bin/activate # macOS/Linux
venv\Scripts\activate # Windows
Tools for Testing
Flask does not generate interactive API documentation. Use one of the following:
- Browser — suitable for GET endpoints only
curl— a command-line HTTP client. Examples are included in the READMEs.- Postman — a GUI application for composing and sending requests
Running Flask Applications
Unlike FastAPI (which uses uvicorn), Flask applications are typically started directly:
python app.py
Flask's built-in development server listens on http://127.0.0.1:5000 by default.
Assignment 01: Basic To-Do API
Location: flask-assignments/01-basic-todo-api/
What You Will Learn
- Flask routing with
@app.routeand themethodsparameter - Accessing the JSON request body
- Returning JSON responses
- Dynamic URL parameters with type converters
- Error handling with
abort
Key Concepts
Flask routing maps URL patterns to Python functions. Unlike FastAPI, Flask uses a single decorator @app.route with a methods list to specify which HTTP methods are accepted. A route without a methods argument defaults to GET only.
request.json is a parsed representation of the incoming JSON body. If the request has no Content-Type: application/json header or the body is not valid JSON, request.json is None. Unlike FastAPI (which validates the shape automatically), Flask requires you to validate manually.
jsonify() creates a Response object with Content-Type: application/json and the data serialised as JSON. Returning a plain Python dict from a Flask route also works in modern Flask, but jsonify() is more explicit and allows you to specify the HTTP status code: return jsonify(data), 201.
abort(404) raises an HTTPException that Flask converts into an error response. By default, Flask returns an HTML error page. To return JSON errors, register an error handler: @app.errorhandler(404).
Setup
pip install Flask
Running the Server
python app.py
Project Files
app.py
Contains everything: the application instance, in-memory storage, and all route definitions.
app = Flask(__name__)
__name__ tells Flask the name of the current module, which is used to locate resources (templates, static files).
The todos list and next_id counter are module-level globals. Flask is single-threaded by default in development mode, so concurrent access is not a concern here. In production, multiple workers would cause race conditions — a real application uses a database.
Route definition pattern:
@app.route('/todos/<int:todo_id>', methods=['PUT'])
def update_todo(todo_id):
...
<int:todo_id> is a URL converter. Flask extracts todo_id from the URL, converts it to an integer, and passes it as an argument to the function. Other converters: <string:name>, <float:value>, <path:filepath>.
Testing Step by Step
# Create a task
curl -X POST http://127.0.0.1:5000/todos \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries"}'
# List all tasks
curl http://127.0.0.1:5000/todos
# Update task with id 1
curl -X PUT http://127.0.0.1:5000/todos/1 \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries and milk"}'
# Delete task with id 1
curl -X DELETE http://127.0.0.1:5000/todos/1
Common Issues
405 Method Not Allowed— the route exists but does not accept the HTTP method you used. Check themethodslist in@app.route.400 Bad Request/Nonefromrequest.json— theContent-Type: application/jsonheader is missing from your request.
Assignment 02: Personal Blog
Location: flask-assignments/02-personal-blog/
What You Will Learn
- Flask-SQLAlchemy for database integration
- Defining SQLAlchemy models as Python classes
- Creating and querying database records
- Converting model objects to JSON-serialisable dicts
Key Concepts
Flask-SQLAlchemy is an extension that integrates SQLAlchemy with Flask. It provides a db object that manages the engine, session, and model base class. It also handles session lifecycle automatically — the session is created at the start of a request and torn down at the end.
The Model class in Flask-SQLAlchemy provides a query attribute for database queries. Common patterns:
Post.query.all()— select all rowsPost.query.filter_by(slug=slug).first()— select by a specific column value, return the first result orNonePost.query.get(id)— select by primary key
db.session.add(obj) stages an object for insertion. db.session.commit() writes the staged changes to the database. If commit() raises an exception (e.g., a unique constraint violation), call db.session.rollback() to reset the session.
Setup
pip install Flask Flask-SQLAlchemy
Running the Server
python app.py
SQLAlchemy creates blog.db on first run.
Project Files
app.py
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db = SQLAlchemy(app)
db.create_all() is called inside with app.app_context() to ensure the application context is active when the tables are created.
models.py
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
slug = db.Column(db.String(200), unique=True, nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return { "id": self.id, "title": self.title, ... }
unique=True on the slug column creates a database-level uniqueness constraint. Even if application code misses the check, the database will reject duplicates.
to_dict() converts the SQLAlchemy object to a plain Python dict. This is necessary because jsonify() cannot serialise SQLAlchemy objects directly.
Testing Step by Step
# Create a post
curl -X POST http://127.0.0.1:5000/posts \
-H "Content-Type: application/json" \
-d '{"title": "Hello World", "slug": "hello-world", "content": "First post."}'
# Retrieve by slug
curl http://127.0.0.1:5000/posts/hello-world
# Stop and restart the server, then retrieve again
# The post still exists — SQLite persisted it
Assignment 03: User Profile & Uploads
Location: flask-assignments/03-user-profile-uploads/
What You Will Learn
- File uploads in Flask via
request.files - Securing uploaded filenames
- Flask's static file serving
- Accepting form fields via
request.form
Key Concepts
request.files is a dictionary of uploaded files. Each value is a FileStorage object (from Werkzeug, Flask's underlying library). FileStorage provides:
.filename— the original name from the client.content_type— MIME type.save(path)— write the file to a local path
secure_filename(filename) from werkzeug.utils sanitises filenames. It removes path separators and other characters that could cause a path traversal attack. secure_filename("../../etc/passwd") returns "etc_passwd".
Flask static files: When static_folder is set (default: "static"), Flask serves files from that directory under the /static/ URL prefix. A file at static/uploads/avatar.png is accessible at http://localhost:5000/static/uploads/avatar.png.
request.form contains fields submitted as application/x-www-form-urlencoded or multipart/form-data. Use request.form.get("field_name") to retrieve a value (returns None if absent, avoiding a KeyError).
Setup
pip install Flask
Werkzeug (which provides secure_filename) is installed automatically as a Flask dependency.
Running the Server
python app.py
Testing Step by Step
# Get current profile
curl http://127.0.0.1:5000/profiles/john_doe
# Upload an image
curl -X POST -F "file=@./photo.jpg" \
http://127.0.0.1:5000/profiles/john_doe/upload-image
# Update bio via form
curl -X PATCH -F "bio=Python developer" \
http://127.0.0.1:5000/profiles/john_doe
After uploading, open the returned profile_image_url in your browser to confirm the image is served.
Assignment 04: Task Manager with Background Tasks
Location: flask-assignments/04-task-manager-bg-tasks/
What You Will Learn
- Background task execution using Python's
threadingmodule - The contrast between Flask's synchronous model and FastAPI's async model
- When threading is appropriate and when a task queue is necessary
Key Concepts
Flask is synchronous by default. Each request handler runs from start to finish on a single thread before the response is sent. There is no built-in equivalent of FastAPI's BackgroundTasks.
Python's threading.Thread creates a new OS thread. By calling thread.start() without thread.join(), the main thread (which handles the HTTP request) continues without waiting for the background thread to finish. The HTTP response is sent immediately; the background thread runs concurrently.
Limitations of threading in Flask:
- Global Interpreter Lock (GIL): Python's GIL prevents true parallel CPU execution, but I/O-bound tasks (network calls, file writes) release the GIL and run effectively concurrently.
- No persistence: If the process crashes, in-flight background tasks are lost.
- No retry logic: If the simulated email send fails, nothing retries it.
For production workloads, use a dedicated task queue: Celery (with Redis or RabbitMQ as a broker) is the standard choice for Flask applications. Celery persists tasks, retries on failure, and provides monitoring.
Setup
pip install Flask
threading is part of the Python standard library.
Running the Server
python app.py
Project Files
app.py
The complete_task endpoint:
thread = threading.Thread(target=send_task_notification,
kwargs={"email": task["user_email"],
"task_title": task["title"]})
thread.start()
return jsonify({"id": task_id, "completed": True}), 200
The thread is started with thread.start(). The response is returned immediately after — the background thread continues independently.
email_utils.py
def send_task_notification(email: str, task_title: str):
logging.info("Starting background task...")
time.sleep(5)
logging.info(f"Notification SENT to {email}: task '{task_title}' completed.")
Testing Step by Step
- Start the server with the terminal visible.
- Create a task:
curl -X POST http://127.0.0.1:5000/tasks -H "Content-Type: application/json" -d '{"title": "Deploy app", "user_email": "dev@example.com"}' - Note the task ID (e.g.,
1). - Complete it:
curl -X POST http://127.0.0.1:5000/tasks/1/complete - The response arrives immediately. Five seconds later, the terminal logs
Notification SENT to dev@example.com.
Assignment 05: Weather Dashboard
Location: flask-assignments/05-weather-dashboard/
What You Will Learn
- Making synchronous HTTP requests from Flask using the
requestslibrary - Parsing external API JSON responses
- Building an in-memory cache with TTL
Key Concepts
The requests library is the standard synchronous HTTP client for Python. It provides a simple interface: requests.get(url, params=dict). Unlike httpx (used in the FastAPI version), requests is synchronous — the thread blocks until the response arrives.
Synchronous vs asynchronous I/O: In Flask, blocking on requests.get(...) means the thread cannot handle other requests while waiting. For low-traffic learning applications this is fine. For high-concurrency production systems, asynchronous I/O (or multiple worker processes) is required.
requests.raise_for_status() raises an exception if the HTTP status code indicates an error (4xx or 5xx). Without this call, a failed API response would silently return an unusable response object.
request.args.get() retrieves query parameters from the URL. For the URL /weather?city_name=London&lat=51.5, request.args.get("city_name") returns "London". A missing parameter returns None (or a default if specified).
Setup
pip install Flask requests
Running the Server
python app.py
An active internet connection is required.
Project Files
weather_service.py
class WeatherService:
def get_weather(self, lat, lon):
url = "https://api.open-meteo.com/v1/forecast"
params = {"latitude": lat, "longitude": lon, "current_weather": True}
response = requests.get(url, params=params)
response.raise_for_status()
return response.json()
app.py
Cache structure:
weather_cache = {}
# Key: (round(lat, 1), round(lon, 1))
# Value: {"data": {...}, "timestamp": float}
The TTL check: time.time() - entry["timestamp"] < 900 (15 minutes).
Testing Step by Step
# First request — real API call
curl "http://127.0.0.1:5000/weather?city_name=London&lat=51.5&lon=-0.12"
# Response includes "cached": false
# Second request — from cache
curl "http://127.0.0.1:5000/weather?city_name=London&lat=51.5&lon=-0.12"
# Response includes "cached": true
Assignment 06: Real-time Chat
Location: flask-assignments/06-real-time-chat/
What You Will Learn
- WebSockets in Flask using Flask-SocketIO
- The Socket.IO protocol vs raw WebSockets
- Room-based event broadcasting
- The WSGI/ASGI distinction and why Flask needs an async worker for WebSockets
Key Concepts
Flask-SocketIO adds WebSocket (and Socket.IO) support to Flask. Raw Flask uses WSGI (Web Server Gateway Interface), which is synchronous and does not support long-lived connections natively. Flask-SocketIO requires an async backend like eventlet or gevent to monkey-patch Python's I/O operations.
Socket.IO is a protocol built on top of WebSockets (with HTTP long-polling as a fallback). It adds rooms, namespaces, and reliable event delivery. The JavaScript client must use the socket.io.js library; a raw WebSocket client will not work.
Events: Socket.IO uses named events instead of raw message streams. @socketio.on('chat_message') registers a handler for events named chat_message. The JavaScript client emits events with socket.emit('chat_message', text).
emit(event, data, broadcast=True, include_self=False) sends an event to clients. broadcast=True sends to everyone in the default namespace. include_self=False prevents the sender from receiving their own message.
Setup
pip install Flask Flask-SocketIO eventlet
Eventlet is the asynchronous backend. It monkey-patches Python's standard networking library so that blocking calls become non-blocking.
Running the Server
python app.py
Note: use socketio.run(app) inside app.py, not the standard app.run().
Project Files
app.py
socketio = SocketIO(app, async_mode='eventlet', cors_allowed_origins='*')
@socketio.on('join')
def handle_join(data):
client_id = data['client_id']
emit('system', f'Client {client_id} joined', broadcast=True)
@socketio.on('chat_message')
def handle_message(data):
emit('chat_message', data, broadcast=True, include_self=False)
@socketio.on('disconnect')
def handle_disconnect():
emit('system', 'A client disconnected', broadcast=True)
The @socketio.on('disconnect') handler is called automatically when a client disconnects.
Testing Step by Step
- Start the server.
- Open
http://127.0.0.1:5000/in one tab, then a second tab. - Each tab shows a unique client ID.
- Type in one tab and click "Send". The message appears in both tabs.
- Close one tab. The other shows a disconnection notice.
Common Issues
RuntimeError: The server is not configured to use long-polling— eventlet is not installed.ImportError: cannot import name 'SocketIO'— Flask-SocketIO is not installed.
Assignment 07: Form Handling & Jinja2 Templates
Location: flask-assignments/07-forms-jinja2/
What You Will Learn
- Flask's template system (Jinja2)
render_templatevsrender_template_string- Form processing with
request.form - Server-side validation and error message rendering
- Pre-filling form fields after a failed submission
Key Concepts
Flask uses Jinja2 natively. Templates are stored in a templates/ directory relative to app.py. render_template("index.html", name=name, errors=errors) renders the template file and passes the variables into the Jinja2 context.
GET/POST form pattern in Flask:
GET /— render the empty formPOST /subscribe— process the submission
This is implemented by registering two routes with different methods. An alternative is one route with methods=["GET", "POST"] and a conditional on request.method.
Jinja2 template syntax recap:
{{ variable }}— output a variable{% if condition %}...{% endif %}— conditional block{% for item in list %}...{% endfor %}— loop{{ value if value else '' }}— inline conditional (ternary)
Setup
pip install Flask
Jinja2 is a Flask dependency and is installed automatically.
Running the Server
python app.py
Project Files
app.py
@app.route('/subscribe', methods=['POST'])
def subscribe():
name = request.form.get('name', '')
email = request.form.get('email', '')
errors = {}
if len(name) < 2:
errors['name'] = 'Name must be at least 2 characters.'
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
errors['email'] = 'Please enter a valid email address.'
if errors:
return render_template('index.html', name=name, email=email, errors=errors), 422
# valid — store and show success
subscribers.append({"name": name, "email": email})
return render_template('success.html', name=name, email=email)
The 422 status code indicates the server understood the request but rejected it due to validation failure. Returning 400 is also acceptable but less precise.
templates/index.html
<input name="name" value="{{ name if name else '' }}">
{% if errors.name %}
<p class="error">{{ errors.name }}</p>
{% endif %}
errors.name in Jinja2 accesses the name key of the errors dictionary. This is equivalent to errors["name"] in Python, but Jinja2 uses dot notation for both attribute access and dictionary key access.
Testing Step by Step
- Open
http://127.0.0.1:5000/. - Submit a valid name (3+ characters) and valid email. Observe the success page.
- Submit with name "A". Observe the error message and note that the email field retains your input.
- Submit with email "invalid". Observe the error message.
Assignment 08: Webhook Receiver
Location: flask-assignments/08-webhook-receiver/
What You Will Learn
- Accessing the raw request body in Flask
- HMAC signature verification
- Constant-time comparison with
hmac.compare_digest - The importance of verifying webhooks before parsing them
Key Concepts
See Assignment 08 in the FastAPI guide for a detailed explanation of webhooks, HMAC, and timing attacks. The cryptographic concepts are identical across all frameworks. The Flask-specific detail is:
request.data in Flask contains the raw request body as bytes. This is what must be used for HMAC verification. If you read request.json instead, Flask parses the body and the original byte sequence is no longer accessible. Always read request.data first, then call json.loads(request.data) manually.
abort(401) raises an HTTPException with status 401 Unauthorized. Flask converts this to an error response. You can customise the response format with an error handler:
@app.errorhandler(401)
def unauthorised(e):
return jsonify({"error": "Invalid signature"}), 401
Setup
pip install Flask
All cryptographic functions (hmac, hashlib) are standard library modules.
Testing Step by Step
Use the Python test script from the assignment README. Key verification:
- Send a correctly signed request →
200 OKand the event is logged. - Modify the
secretin the test script →401 Unauthorized. - Remove the
x-webhook-signatureheader from the request →401 Unauthorized. GET /admin/eventsto view logged events.
Assignment 09: Inventory Management
Location: flask-assignments/09-inventory-management/
What You Will Learn
- Flask Blueprints for code organisation and URL prefixing
- Implementing API versioning with separate modules
- Offset/limit pagination
- Structured vs simple pagination responses
Key Concepts
Flask Blueprints are modular components for organising a Flask application. Each Blueprint can define routes, error handlers, and static files. The main application registers blueprints, optionally with a URL prefix:
app.register_blueprint(v1_bp, url_prefix='/v1')
app.register_blueprint(v2_bp, url_prefix='/v2')
Routes defined inside a Blueprint are prefixed with the registration URL. A route @v1_bp.route('/items') in v1/routes.py becomes /v1/items in the application.
Blueprints vs FastAPI's APIRouter: The concept is identical — both allow splitting a large application into smaller, focused modules with their own URL prefixes.
Setup
pip install Flask
Running the Server
python app.py
Project Files
v1/routes.py
v1_bp = Blueprint('v1', __name__)
@v1_bp.route('/items', methods=['GET'])
def list_items_v1():
offset = int(request.args.get('offset', 0))
limit = int(request.args.get('limit', 20))
return jsonify(inventory_data[offset : offset + limit])
Returns a raw JSON array — no metadata.
v2/routes.py
v2_bp = Blueprint('v2', __name__)
@v2_bp.route('/items', methods=['GET'])
def list_items_v2():
offset = int(request.args.get('offset', 0))
limit = int(request.args.get('limit', 20))
category = request.args.get('category')
filtered = [i for i in inventory_data if not category or i['category'] == category]
total = len(filtered)
items = filtered[offset : offset + limit]
return jsonify({
"items": items,
"total": total,
"offset": offset,
"limit": limit,
"has_more": offset + limit < total
})
app.py
from v1.routes import v1_bp
from v2.routes import v2_bp
app.register_blueprint(v1_bp, url_prefix='/v1')
app.register_blueprint(v2_bp, url_prefix='/v2')
Testing Step by Step
# V1: plain array
curl "http://127.0.0.1:5000/v1/items?offset=0&limit=5"
# V2: structured response with metadata
curl "http://127.0.0.1:5000/v2/items?offset=0&limit=5"
# V2: filtered by category
curl "http://127.0.0.1:5000/v2/items?category=Electronics&offset=0&limit=10"
Assignment 10: Secure Document Vault
Location: flask-assignments/10-secure-document-vault/
What You Will Learn
- JWT authentication in Flask using
PyJWT - Password hashing with
passliband bcrypt - Custom decorator-based authorisation
- Bearer token extraction from the
Authorizationheader - Scope-based and clearance-level-based access control
Key Concepts
See Assignment 10 in the FastAPI guide for conceptual background on JWT, password hashing, and RBAC. The Flask implementation differs in the mechanism of enforcement:
Custom decorators: Flask does not have a built-in Security(...) system like FastAPI. Access control is implemented as a Python decorator that wraps route functions. The decorator:
- Reads the
Authorizationheader:request.headers.get("Authorization") - Extracts the token: the header value has the format
Bearer <token> - Decodes the JWT using
PyJWT - Checks required scopes
- Attaches the user to the request context:
request.user = user - Calls the original function if everything is valid; returns a 401/403 otherwise
def token_required(required_scopes=None):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return jsonify({"error": "Missing token"}), 401
token = auth_header.split(" ")[1]
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
# scope check, user attachment...
return f(*args, **kwargs)
return decorated
return decorator
@wraps(f) from functools preserves the original function's name and docstring, which is important for Flask's routing system.
Setup
pip install Flask PyJWT passlib[bcrypt]
PyJWT— JWT encoding/decodingpasslib[bcrypt]— password hashing
Running the Server
python app.py
Testing Step by Step
# Authenticate as alice
curl -X POST http://127.0.0.1:5000/token \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "alice123"}'
# Copy the access_token from the response
# Read documents (requires vault:read scope)
curl http://127.0.0.1:5000/documents \
-H "Authorization: Bearer <your_token>"
# Create a document with secret_level above alice's clearance (will fail)
curl -X POST http://127.0.0.1:5000/documents \
-H "Authorization: Bearer <your_token>" \
-H "Content-Type: application/json" \
-d '{"title": "Top Secret", "content": "...", "secret_level": 4}'
# Expect 403 Forbidden
# Try admin endpoint as alice (will fail — wrong scope)
curl http://127.0.0.1:5000/admin/users \
-H "Authorization: Bearer <your_token>"
# Expect 403 Forbidden
Repeat with admin / admin123 to confirm that admin-level access works.