Flask Forms: Input, Validation, and Flash Messages

• Python • By TryzTech Team
FlaskPythonBackendWeb Development

As soon as an application accepts a form, the data is no longer fully under our control. A field can be empty, contain only spaces, be missing from the request, or be much longer than expected. The backend must therefore validate input even when the HTML uses attributes such as required.

The following example builds an add-task form with a common flow: the page opens with GET, data is submitted with POST, and the server redirects after processing it.

Understand GET, POST, and Redirect

When a user opens /tasks/new, the browser sends GET and Flask renders the form. After the Save button is pressed, the browser sends POST /tasks with the input. The create route validates the data, stores it when valid, and redirects the browser back to the form.

This sequence is often called the Post/Redirect/Get pattern. The redirect makes the browser’s latest request a GET again. Refreshing the page therefore does not resend the POST or create the same task twice.

Handle the Form in Flask

For now, the tasks live in a list so the example can focus on form handling. A database can replace this list later without changing the validation concept.

# File: app.py
from flask import Flask, flash, redirect, render_template, request, url_for

app = Flask(__name__)
app.secret_key = "dev-secret"
tasks = []


@app.get("/tasks/new")
def new_task():
    return render_template("new_task.html")


@app.post("/tasks")
def create_task():
    title = request.form.get("title", "").strip()

    if not title:
        flash("A task title is required.", "error")
        return redirect(url_for("new_task"))

    if len(title) > 100:
        flash("A task title cannot exceed 100 characters.", "error")
        return redirect(url_for("new_task"))

    tasks.append(title)
    flash("Task created successfully.", "success")
    return redirect(url_for("new_task"))

request.form.get("title", "") is more forgiving than request.form["title"]. If the title field is missing, .get() returns an empty string, allowing the application to show a validation message instead of failing with BadRequestKeyError.

The .strip() method removes leading and trailing whitespace. Without it, input containing only spaces would pass if not title even though it has no useful content.

flash() stores a message for the next request. Flash messages use the session, so Flask needs a secret_key. The value "dev-secret" is suitable only for local practice; production should read a long, random secret from an environment variable.

Render the Form and Its Messages

The template must render flash messages explicitly. Calling flash() in a route without reading the messages in a template gives the user no visible feedback.

<!-- File: templates/new_task.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Add Task</title>
  </head>
  <body>
    {% with messages = get_flashed_messages(with_categories=true) %}
      {% for category, message in messages %}
        <p class="message {{ category }}">{{ message }}</p>
      {% endfor %}
    {% endwith %}

    <h1>Add Task</h1>

    <form method="post" action="{{ url_for('create_task') }}">
      <label for="title">Title</label>
      <input id="title" name="title" maxlength="100" required>
      <button type="submit">Save</button>
    </form>
  </body>
</html>

The action="{{ url_for('create_task') }}" expression generates a URL from the route function name. It is more resilient than writing /tasks in several templates. If the route path changes, the template still points to the correct endpoint.

The error and success categories are stored with each message. They can later become CSS classes that visually distinguish failed and successful feedback.

Browser Validation Does Not Replace Server Validation

The required and maxlength attributes help users complete a form correctly, but they run in the browser and can be bypassed. A request may also arrive from curl, a script, or an API tool without using the HTML form. Route-level validation remains the source of truth.

Forms that change important data also need CSRF protection. Flask-WTF is one common option. This example omits it so the basic request and validation flow remains visible, so do not copy it unchanged into a production form that modifies sensitive data.

Try Several Inputs

  • Submit the form without a title and confirm that an error appears.
  • Send a title containing only spaces.
  • Enter more than 100 characters.
  • Add an optional description field.
  • After a successful submission, redirect to a task list page.

As you try these cases, notice that a good form does more than accept input. It explains what went wrong, keeps refresh behavior safe, and never assumes that client data is automatically valid.

Keep reading within the same topic.

Don't Miss Out

Get the latest tech articles, tips, and insights delivered to your inbox.