Redirect Chapter 11: Web Development with Flask | AI Fundamentals
← Back to Tutorials Chapter 11

Web Development with Flask

Flask is a lightweight web framework for Python. It lets you turn a few functions into a live website or an API with remarkably little code, while still giving you the freedom to structure larger projects however you like. This chapter builds from a minimal app to templated pages, forms, and a small REST API.

Introduction to the Flask Framework

A web framework maps URLs to Python functions. When a browser requests /about, Flask calls the function decorated with @app.route("/about") and sends its return value back to the browser. Flask is "micro" — it does the core job well and leaves databases, forms, and extensions to you, chosen as needed.

Install it once per environment:

pip install flask

A Simple Flask App Skeleton

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "<h1>Hello from Flask</h1>"

@app.route("/about")
def about():
    return "<p>A page about this project.</p>"

if __name__ == "__main__":
    app.run(debug=True)

Run the file, then open http://127.0.0.1:5000 in your browser. Flask starts a development server and, with debug=True, reloads automatically whenever you edit the code.

Keep HTML out of Python strings. Returning raw HTML from functions works for a demo, but it becomes unreadable fast. The template engine in the next section is the proper way to mix Python data with HTML.

Integrating HTML: Templates and Static Files

Flask looks for templates in a templates/ folder and for CSS, images, and JavaScript in a static/ folder, both next to your app file.

render_template and the templates Folder

from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def home():
    name = "Ada"
    return render_template("home.html", name=name)

if __name__ == "__main__":
    app.run(debug=True)

templates/home.html:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <h1>Welcome, {{ name }}</h1>
</body>
</html>

The double braces {{ name }} print a value passed from Python, and url_for builds correct links to files inside static/.

HTTP Verbs: GET and POST

Browsers send GET requests to fetch a page and POST requests to submit data. A search form is a classic mix: GET to show the form, then a GET with a query string, or POST when the data should not appear in the URL.

from flask import Flask, request

app = Flask(__name__)

@app.route("/search", methods=["GET", "POST"])
def search():
    if request.method == "POST":
        term = request.form.get("q", "")
        return f"<p>You searched for: {term}</p>"
    return "<form method='POST'><input name='q'><button>Search</button></form>"

if __name__ == "__main__":
    app.run(debug=True)

request.form holds POST body values; request.args holds the values from a GET query string.

Dynamic URLs, Variable Rules, and Jinja2

Parts of a URL can be captured as variables using angle brackets, then used as function arguments.

@app.route("/user/<name>")
def profile(name):
    return f"<p>Profile of {name}</p>"

@app.route("/posts/<int:post_id>")
def post(post_id):
    return f"<p>Showing post number {post_id}</p>"

The int: converter ensures the argument is an integer; otherwise Flask returns a 404.

Jinja2 Template Engine

Jinja2, bundled with Flask, adds control flow to templates.

@app.route("/scores")
def scores():
    data = {"Mia": 88, "Noah": 72, "Leo": 95}
    return render_template("scores.html", scores=data)

templates/scores.html:

<ul>
{% for name, score in scores.items() %}
    <li>{{ name }}:
        {% if score >= 90 %}Excellent{% elif score >= 75 %}Good{% else %}Needs work{% endif %}
        ({{ score }})</li>
{% endfor %}
</ul>

REST APIs: PUT and DELETE, JSON In and Out

Beyond HTML pages, Flask serves data to other programs as JSON. A REST API exposes resources and maps the verbs GET (read), POST (create), PUT (update), and DELETE (remove) onto them.

from flask import Flask, request, jsonify

app = Flask(__name__)
tasks = {}          # simple in-memory store: id -> title
next_id = 1

@app.route("/tasks", methods=["GET"])
def list_tasks():
    return jsonify(tasks)

@app.route("/tasks", methods=["POST"])
def create_task():
    global next_id
    body = request.get_json()
    tasks[next_id] = body.get("title", "untitled")
    reply = {"id": next_id, "title": tasks[next_id]}
    next_id += 1
    return jsonify(reply), 201

@app.route("/tasks/<int:tid>", methods=["PUT"])
def update_task(tid):
    body = request.get_json()
    if tid not in tasks:
        return jsonify({"error": "not found"}), 404
    tasks[tid] = body.get("title", tasks[tid])
    return jsonify({"id": tid, "title": tasks[tid]})

@app.route("/tasks/<int:tid>", methods=["DELETE"])
def delete_task(tid):
    if tid not in tasks:
        return jsonify({"error": "not found"}), 404
    del tasks[tid]
    return jsonify({"deleted": tid}), 200

if __name__ == "__main__":
    app.run(debug=True)

request.get_json() parses the incoming JSON body, and jsonify serializes a Python dict into a proper JSON response with the right content type. Status codes (201 created, 404 missing, 200 success) tell the caller what happened.

Test the API without a browser: Use the curl command or a tool like Postman. For example, curl -X POST -H "Content-Type: application/json" -d '{"title":"buy milk"}' http://127.0.0.1:5000/tasks creates a task, and curl -X DELETE http://127.0.0.1:5000/tasks/1 removes it.

Practice Exercise

Build a small note-taking app with three routes:

  1. GET / renders a template showing all saved notes.
  2. POST /add accepts a note text from a form and stores it.
  3. POST /delete/<int:nid> removes a note by id and redirects home.
from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)
notes = []
next_id = 1

@app.route("/")
def home():
    return render_template("notes.html", notes=notes)

@app.route("/add", methods=["POST"])
def add():
    global next_id
    text = request.form.get("text", "").strip()
    if text:
        notes.append({"id": next_id, "text": text})
        next_id += 1
    return redirect(url_for("home"))

@app.route("/delete/<int:nid>", methods=["POST"])
def delete(nid):
    global notes
    notes = [n for n in notes if n["id"] != nid]
    return redirect(url_for("home"))

if __name__ == "__main__":
    app.run(debug=True)

templates/notes.html:

<h1>My Notes</h1>
<form method="POST" action="{{ url_for('add') }}">
    <input name="text" placeholder="New note...">
    <button>Save</button>
</form>
<ul>
{% for note in notes %}
    <li>{{ note.text }}
        <form method="POST" action="{{ url_for('delete', nid=note.id) }}">
            <button>Delete</button>
        </form>
    </li>
{% else %}
    <li>No notes yet.</li>
{% endfor %}
</ul>
Stretch goal: Change the in-memory notes list to a SQLite table using the sqlite3 skills from Chapter 9. Add a GET /notes.json route that returns all notes as JSON using jsonify, turning your note app into both a website and a tiny REST API.