Flask Templates and Forms

Jinja2 templates, template inheritance, handling form submissions, and basic input validation.

Rendering templates with Jinja2

Flask uses the Jinja2 templating engine to generate HTML. By convention, templates live in a templates/ folder next to your application file, and you render them with render_template:

Python
from flask import render_template

@app.route("/hello/<name>")
def hello(name):
    return render_template("hello.html", name=name)
HTML
<!-- templates/hello.html -->
<!DOCTYPE html>
<html>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

{{ name }} inserts the value passed in from the view. Jinja2 auto-escapes any HTML special characters in that value by default (<, >, &, quotes) — so if name were <script>alert(1)</script>, it would render as inert, visible text rather than executing as a script. This is what protects Flask apps from a large class of cross-site scripting (XSS) bugs by default.

Template inheritance

Real pages share a common shell — a header, navigation, a footer — and Jinja2's {% extends %} / {% block %} pair lets you define that shell once and fill in only what changes per page.

HTML
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My App{% endblock %}</title>
</head>
<body>
    <nav>Home | About | Contact</nav>
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>
HTML
<!-- templates/home.html -->
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
    <h1>Welcome!</h1>
    <p>This page only defines the parts that differ from the base layout.</p>
{% endblock %}

{% extends "base.html" %} must be the first line of the child template. Every {% block %} in base.html is a slot a child template can override; a block a child doesn't override just falls back to the base's own content — useful for a default title that only a handful of pages need to change.

Jinja2 also supports control flow directly in templates:

HTML
{% if user %}
    <p>Welcome back, {{ user.name }}!</p>
{% else %}
    <p><a href="/login">Log in</a></p>
{% endif %}

<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

Handling form submissions

A standard HTML form submits to a route, which Flask reads via request.form:

HTML
<!-- templates/contact.html -->
<form method="post" action="/contact">
    <input type="email" name="email" placeholder="you@example.com">
    <button type="submit">Send</button>
</form>
Python
from flask import render_template, request

@app.route("/contact", methods=["GET", "POST"])
def contact():
    if request.method == "POST":
        email = request.form.get("email")
        # ... process the submission (save it, email it, etc.)
        return render_template("thank_you.html")
    return render_template("contact.html")

The same view handles both the initial GET (show the empty form) and the POST (process the submitted data) — a very common Flask pattern for simple forms.

Basic input validation

For anything beyond a trivial form, validate before trusting the data. flash() is Flask's built-in mechanism for passing a one-time message through a redirect — commonly used to show a validation error back on the same form:

Python
from flask import flash, redirect, render_template, request, url_for

app.secret_key = "change-this-to-a-real-secret-in-production"  # required for flash/session

@app.route("/contact", methods=["GET", "POST"])
def contact():
    if request.method == "POST":
        email = request.form.get("email", "").strip()

        if not email or "@" not in email:
            flash("Please enter a valid email address.")
            return redirect(url_for("contact"))

        # email is valid — go ahead and process it
        return redirect(url_for("thank_you"))

    return render_template("contact.html")
HTML
<!-- templates/contact.html -->
{% with messages = get_flashed_messages() %}
    {% if messages %}
        <ul class="errors">
        {% for message in messages %}
            <li>{{ message }}</li>
        {% endfor %}
        </ul>
    {% endif %}
{% endwith %}

<form method="post" action="/contact">
    <input type="email" name="email" placeholder="you@example.com">
    <button type="submit">Send</button>
</form>

Manual validation like this is fine for a single simple field. For anything with several fields, file uploads, or CSRF protection, most real Flask apps reach for the Flask-WTF extension instead, which wraps WTForms and handles CSRF tokens, field-level validators, and re-populating the form with previously entered values automatically.

Common mistakes

  • Skipping CSRF protection on real forms — a plain HTML form with no CSRF token is vulnerable to cross-site request forgery; Flask-WTF adds this automatically, hand-rolled forms need it added manually.
  • Using the |safe filter (which disables Jinja2's auto-escaping) on user-supplied input "just to get the HTML to render" — this reopens the exact XSS hole auto-escaping exists to close.
  • Forgetting to .strip() and check for emptiness before validating — a field containing only whitespace passes a naive if email: check.

Interview questions

Q: What does Jinja2's auto-escaping protect against, and when would you disable it? Auto-escaping converts HTML-significant characters (<, >, &, quotes) in template variables into their safe entity equivalents before rendering, preventing user-supplied text from being interpreted as HTML or JavaScript — the core defense against stored/reflected XSS. You'd disable it (via the |safe filter or Markup) only for content you trust completely, such as HTML you generated and sanitized yourself — never for raw user input.

Q: What's the difference between {% extends %} and {% include %} in Jinja2? {% extends %} establishes a parent/child relationship for a whole page — the child fills in named {% block %} slots defined by the parent layout, and only one extends is allowed per template. {% include %} simply pastes another template's rendered output inline at that point, with no block structure involved — useful for a reusable snippet like a shared form field or a card component, rather than a whole-page layout.