1 Flask Assignments
teacher edited this page 2026-04-13 13:46:45 +02:00

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.route and the methods parameter
  • 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 the methods list in @app.route.
  • 400 Bad Request / None from request.json — the Content-Type: application/json header 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 rows
  • Post.query.filter_by(slug=slug).first() — select by a specific column value, return the first result or None
  • Post.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 threading module
  • 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

  1. Start the server with the terminal visible.
  2. 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"}'
  3. Note the task ID (e.g., 1).
  4. Complete it: curl -X POST http://127.0.0.1:5000/tasks/1/complete
  5. 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 requests library
  • 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

  1. Start the server.
  2. Open http://127.0.0.1:5000/ in one tab, then a second tab.
  3. Each tab shows a unique client ID.
  4. Type in one tab and click "Send". The message appears in both tabs.
  5. 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_template vs render_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 form
  • POST /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

  1. Open http://127.0.0.1:5000/.
  2. Submit a valid name (3+ characters) and valid email. Observe the success page.
  3. Submit with name "A". Observe the error message and note that the email field retains your input.
  4. 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:

  1. Send a correctly signed request → 200 OK and the event is logged.
  2. Modify the secret in the test script → 401 Unauthorized.
  3. Remove the x-webhook-signature header from the request → 401 Unauthorized.
  4. GET /admin/events to 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 passlib and bcrypt
  • Custom decorator-based authorisation
  • Bearer token extraction from the Authorization header
  • 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:

  1. Reads the Authorization header: request.headers.get("Authorization")
  2. Extracts the token: the header value has the format Bearer <token>
  3. Decodes the JWT using PyJWT
  4. Checks required scopes
  5. Attaches the user to the request context: request.user = user
  6. 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/decoding
  • passlib[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.