Chapter 40: Web Development with Flask
Flask is a web framework for Python. It lets you turn Python functions into web pages and APIs with just a few lines of code.
You already know Python. With Flask, that's enough to build a real website.
What is Flask?
Flask is called a "micro-framework." Micro doesn't mean small or weak — it means Flask gives you the essentials and lets you add only what you need. No mandatory ORM, no required project structure, no magic configuration files.
Compare that to Django, which comes with everything built-in. Flask is lighter, easier to learn, and perfect for APIs and small-to-medium apps.
pip install flask
Your First Web App
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, World!"
@app.route("/about")
def about():
return "This is the about page."
if __name__ == "__main__":
app.run(debug=True)
Run it:
python app.py
Open http://127.0.0.1:5000/ in your browser. You'll see "Hello, World!".
That's it. That's a web server.
@app.route("/") maps the URL path / to the home function. When someone visits that URL, Flask calls your function and sends the return value back as the response.
debug=True enables auto-reload (no need to restart when you edit code) and a browser debugger on errors. Never use it in production.
Routes and URL Parameters
Static routes
@app.route("/")
def home():
return "Home page"
@app.route("/contact")
def contact():
return "Contact us at hello@example.com"
Dynamic routes — path parameters
@app.route("/user/<username>")
def profile(username):
return f"Profile of {username}"
@app.route("/product/<int:product_id>")
def product(product_id):
# Flask converts product_id to int for you
return f"Product #{product_id}"
@app.route("/price/<float:amount>")
def price(amount):
return f"Price: ${amount:.2f}"
Visit /user/alice -> "Profile of alice"
Visit /product/42 -> "Product #42"
Type converters: string (default), int, float, path (slashes allowed), uuid.
HTTP methods
By default, routes only accept GET. Specify methods to allow others:
from flask import Flask, request
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
return f"Logging in as {username}"
# GET — show the login form
return """
<form method="POST">
<input name="username" placeholder="Username">
<input name="password" type="password" placeholder="Password">
<button type="submit">Log in</button>
</form>
"""
The request Object
When someone calls your route, Flask puts all request data in request:
from flask import request
@app.route("/search")
def search():
# Query string: /search?q=python&page=2
query = request.args.get("q", "") # "python"
page = request.args.get("page", 1, type=int) # 2
return f"Search: '{query}', page {page}"
@app.route("/submit", methods=["POST"])
def submit():
# Form data (application/x-www-form-urlencoded)
name = request.form.get("name")
email = request.form.get("email")
# JSON body (application/json)
data = request.get_json()
# Headers
auth = request.headers.get("Authorization")
# Uploaded files
file = request.files.get("avatar")
return "OK"
HTML Templates with Jinja2
Returning HTML strings from functions gets messy fast. Flask uses Jinja2 — a template engine that keeps HTML in separate files.
Create a templates/ folder next to app.py:
my_app/
├── app.py
└── templates/
├── base.html
├── index.html
└── users.html
templates/base.html — the layout every page inherits:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Site{% endblock %}</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 1rem; }
nav a { margin-right: 1rem; }
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/users">Users</a>
</nav>
<hr>
{% block content %}{% endblock %}
</body>
</html>
templates/index.html — extends the base:
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome, {{ name }}!</h1>
<p>The date is {{ date }}.</p>
{% if items %}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% else %}
<p>No items found.</p>
{% endif %}
{% endblock %}
app.py — render the template:
from flask import Flask, render_template
from datetime import date
app = Flask(__name__)
@app.route("/")
def home():
return render_template(
"index.html",
name="Alice",
date=date.today(),
items=["Python", "Flask", "Jinja2"],
)
Jinja2 syntax:
{{ variable }}— output a value{% if condition %}...{% endif %}— conditionals{% for item in list %}...{% endfor %}— loops{% extends "base.html" %}— template inheritance{% block name %}...{% endblock %}— override sections{{ value | filter }}— filters:upper,lower,length,default("none"),safe
Static Files — CSS and Images
Put static files (CSS, JS, images) in a static/ folder:
my_app/
├── app.py
├── templates/
└── static/
├── style.css
└── logo.png
Reference them in templates using url_for:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo">
url_for generates the correct URL regardless of where you deploy the app.
Forms — Reading POST Data
templates/contact.html:
{% extends "base.html" %}
{% block title %}Contact{% endblock %}
{% block content %}
<h1>Contact Us</h1>
{% if message %}
<p style="color: green;">{{ message }}</p>
{% endif %}
{% for error in errors %}
<p style="color: red;">{{ error }}</p>
{% endfor %}
<form method="POST">
<label>Name<br>
<input type="text" name="name" value="{{ form_data.name or '' }}">
</label><br><br>
<label>Email<br>
<input type="email" name="email" value="{{ form_data.email or '' }}">
</label><br><br>
<label>Message<br>
<textarea name="body">{{ form_data.body or '' }}</textarea>
</label><br><br>
<button type="submit">Send</button>
</form>
{% endblock %}
app.py:
@app.route("/contact", methods=["GET", "POST"])
def contact():
message = ""
errors = []
form_data = {}
if request.method == "POST":
name = request.form.get("name", "").strip()
email = request.form.get("email", "").strip()
body = request.form.get("body", "").strip()
form_data = {"name": name, "email": email, "body": body}
if not name:
errors.append("Name is required.")
if not email or "@" not in email:
errors.append("A valid email is required.")
if not body:
errors.append("Message cannot be empty.")
if not errors:
# In a real app: send an email, save to DB, etc.
message = f"Thanks, {name}! We'll be in touch."
form_data = {}
return render_template(
"contact.html",
message=message,
errors=errors,
form_data=form_data,
)
Flask with SQLite — A Real Task App
Let's build a task manager that stores tasks in SQLite.
task_app/
├── app.py
└── templates/
├── base.html
└── tasks.html
app.py:
import sqlite3
from flask import Flask, render_template, request, redirect, url_for, flash
from pathlib import Path
app = Flask(__name__)
app.secret_key = "change-this-in-production" # required for flash messages
DB_PATH = "tasks.db"
def get_db():
"""Open a database connection."""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # rows behave like dicts
return conn
def init_db():
"""Create tables if they don't exist."""
with get_db() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT 0,
created TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
@app.route("/")
def task_list():
with get_db() as conn:
tasks = conn.execute(
"SELECT * FROM tasks ORDER BY done, created DESC"
).fetchall()
return render_template("tasks.html", tasks=tasks)
@app.route("/add", methods=["POST"])
def add_task():
title = request.form.get("title", "").strip()
if not title:
flash("Task title cannot be empty.", "error")
else:
with get_db() as conn:
conn.execute("INSERT INTO tasks (title) VALUES (?)", (title,))
flash(f"Task '{title}' added.", "success")
return redirect(url_for("task_list"))
@app.route("/toggle/<int:task_id>", methods=["POST"])
def toggle_task(task_id):
with get_db() as conn:
conn.execute(
"UPDATE tasks SET done = NOT done WHERE id = ?", (task_id,)
)
return redirect(url_for("task_list"))
@app.route("/delete/<int:task_id>", methods=["POST"])
def delete_task(task_id):
with get_db() as conn:
conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
flash("Task deleted.", "info")
return redirect(url_for("task_list"))
if __name__ == "__main__":
init_db()
app.run(debug=True)
templates/tasks.html:
{% extends "base.html" %}
{% block title %}Tasks{% endblock %}
{% block content %}
<h1>My Tasks</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, msg in messages %}
<p style="color: {{ 'green' if category == 'success' else 'red' if category == 'error' else 'blue' }}">
{{ msg }}
</p>
{% endfor %}
{% endwith %}
<form method="POST" action="/add">
<input type="text" name="title" placeholder="New task..." required>
<button type="submit">Add</button>
</form>
<ul style="list-style: none; padding: 0; margin-top: 1rem;">
{% for task in tasks %}
<li style="margin: 0.5rem 0; {{ 'opacity: 0.5;' if task.done }}">
<form method="POST" action="/toggle/{{ task.id }}" style="display: inline;">
<button type="submit" title="{{ 'Mark undone' if task.done else 'Mark done' }}">
{{ '[x]' if task.done else '[ ]' }}
</button>
</form>
<span style="{{ 'text-decoration: line-through;' if task.done }}">
{{ task.title }}
</span>
<form method="POST" action="/delete/{{ task.id }}" style="display: inline;">
<button type="submit" onclick="return confirm('Delete this task?')"
style="margin-left: 0.5rem; color: red;">[-]</button>
</form>
</li>
{% endfor %}
</ul>
{% if not tasks %}
<p>No tasks yet. Add one above!</p>
{% endif %}
{% endblock %}
Blueprints — Organizing a Larger App
When your app grows, split it into Blueprints:
my_app/
├── app.py
├── auth/
│ ├── __init__.py
│ └── routes.py
└── tasks/
├── __init__.py
└── routes.py
auth/routes.py:
from flask import Blueprint, render_template, request, redirect, url_for
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
# handle login
return redirect(url_for("tasks.task_list"))
return render_template("auth/login.html")
@auth_bp.route("/logout")
def logout():
return redirect(url_for("auth.login"))
app.py:
from flask import Flask
from auth.routes import auth_bp
from tasks.routes import tasks_bp
app = Flask(__name__)
app.secret_key = "your-secret-key"
app.register_blueprint(auth_bp)
app.register_blueprint(tasks_bp)
if __name__ == "__main__":
app.run(debug=True)
Each blueprint has its own routes, templates, and URL prefix. url_for("auth.login") generates /auth/login.
Building a JSON API with Flask
Flask also works great as a pure API server:
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
# In-memory store (use a database in production)
tasks = [
{"id": 1, "title": "Buy milk", "done": False},
{"id": 2, "title": "Walk dog", "done": True},
]
next_id = 3
@app.route("/api/tasks", methods=["GET"])
def get_tasks():
done = request.args.get("done")
if done is not None:
done_bool = done.lower() == "true"
filtered = [t for t in tasks if t["done"] == done_bool]
return jsonify(filtered)
return jsonify(tasks)
@app.route("/api/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
task = next((t for t in tasks if t["id"] == task_id), None)
if task is None:
abort(404)
return jsonify(task)
@app.route("/api/tasks", methods=["POST"])
def create_task():
global next_id
data = request.get_json()
if not data or "title" not in data:
abort(400, description="'title' is required")
task = {"id": next_id, "title": data["title"], "done": False}
next_id += 1
tasks.append(task)
return jsonify(task), 201
@app.route("/api/tasks/<int:task_id>", methods=["PATCH"])
def update_task(task_id):
task = next((t for t in tasks if t["id"] == task_id), None)
if task is None:
abort(404)
data = request.get_json() or {}
task.update({k: v for k, v in data.items() if k in ("title", "done")})
return jsonify(task)
@app.route("/api/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
global tasks
task = next((t for t in tasks if t["id"] == task_id), None)
if task is None:
abort(404)
tasks = [t for t in tasks if t["id"] != task_id]
return "", 204
@app.errorhandler(404)
def not_found(e):
return jsonify(error=str(e)), 404
@app.errorhandler(400)
def bad_request(e):
return jsonify(error=str(e)), 400
Test it:
# Get all tasks
curl http://127.0.0.1:5000/api/tasks
# Create a task
curl -X POST http://127.0.0.1:5000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn Flask"}'
# Update a task
curl -X PATCH http://127.0.0.1:5000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"done": true}'
# Delete a task
curl -X DELETE http://127.0.0.1:5000/api/tasks/1
Error Handlers
@app.errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404
@app.errorhandler(500)
def internal_error(e):
return render_template("500.html"), 500
Deploying Flask
Option 1: Render (free tier)
- Push your code to GitHub
- Go to render.com -> New Web Service
- Connect your repo
- Set start command:
gunicorn app:app - Done — Render gives you a public URL
pip install gunicorn
Never use app.run() (Flask's dev server) in production. gunicorn is the production server.
Option 2: PythonAnywhere (free tier)
- Upload your files
- Go to the Web tab -> Add new web app
- Choose Flask
- Configure WSGI to point to your
app.py
requirements.txt for deployment
pip freeze > requirements.txt
Include at minimum:
flask
gunicorn
What You Learned in This Chapter
Flask(__name__)creates an app.@app.route("/path")maps URLs to functions.- Return a string for plain text,
render_template()for HTML,jsonify()for JSON. <int:id>in the route captures a typed URL parameter.request.argsfor query strings,request.formfor POST form data,request.get_json()for JSON bodies.- Jinja2 templates live in
templates/. Use{{ }}for values,{% %}for logic,{% extends %}and{% block %}for layouts. - Static files live in
static/. Reference them withurl_for('static', filename='...'). flash()+get_flashed_messages()for one-time user notifications.redirect(url_for("function_name"))sends users to another route.- Blueprints split a large app into independent modules.
- Use
gunicornto deploy — never Flask's dev server.
What's Next?
Chapter 41 covers FastAPI — a modern, high-performance API framework with automatic validation, type hints, and interactive documentation. If Flask is the classic choice, FastAPI is where the industry is heading.