Handling Forms

Reviewed & published by Brayan K

Form handling is the process of accepting data a user types into an HTML form, reading it on the server through request.form, validating it, and responding — the backbone of logins, signups, and search boxes.

Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll build a form, process its submission, validate the input, and use the Post/Redirect/Get pattern to avoid duplicate submissions.

What You'll Learn in This Lesson

1 Showing and Reading a Form

An HTML <form> with method="POST" sends its fields to the server when submitted. Each input's name attribute becomes a key in request.form.

The classic pattern is one route that handles both methods: on a GET it renders the empty form, and on a POST it reads the submitted values. The example below shows a contact form and, when posted, returns a message built from the user's name and email.

# app.py — show a form, then read what was submitted
from flask import Flask, request

app = Flask(__name__)

FORM = """
<form method="POST">
  <input name="name" placeholder="Name">
  <input name="email" placeholder="Email">
  <button type="submit">Send</button>
</form>
"""

@app.route("/contact", methods=["GET", "POST"])
def contact():
    if request.method == "POST":
        # Each input's name attribute is a key in request.form
        name = request.form.get("name", "")
        email = request.form.get("email", "")
        return f"Got it! {name} <{email}> will hear back soon."
    return FORM

# Simulate showing and submitting the form:
with app.test_client() as client:
    print(client.get("/contact").status_code)   # 200 (form shown)
    sent = client.post("/contact", data={"name": "Ada", "email": "ada@x.com"})
    print(sent.data.decode())                    # Got it! Ada <ada@x.com> ...

# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
#     if __name__ == "__main__":
#         app.run(debug=True)

# ✅ Expected output:
# 200
# Got it! Ada <ada@x.com> will hear back soon.

2 Validating Input

Never trust user input. Before you store or use form data, check that required fields are present and look reasonable. When validation fails, re-show the form with an error so the user can fix it.

The example below validates a signup: the username must be non-empty and the password at least eight characters. It collects any problems into a list and only "creates" the account when the list is empty.

# app.py — validate form data before acting on it
from flask import Flask, request

app = Flask(__name__)

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

        errors = []
        if not username:
            errors.append("Username is required.")
        if len(password) < 8:
            errors.append("Password must be at least 8 characters.")

        if errors:
            # Re-show the form area with the problems listed
            return "Please fix: " + " ".join(errors), 400

        return f"Welcome aboard, {username}!"
    return '<form method="POST">' \
           '<input name="username"><input name="password" type="password">' \
           '<button>Join</button></form>'

# Try a bad and a good submission:
with app.test_client() as client:
    bad = client.post("/signup", data={"username": "", "password": "123"})
    print(bad.status_code, bad.data.decode())     # 400 Please fix: ...
    good = client.post("/signup", data={"username": "ada", "password": "longenough"})
    print(good.data.decode())                      # Welcome aboard, ada!

# ✅ Expected output:
# 400 Please fix: Username is required. Password must be at least 8 characters.
# Welcome aboard, ada!

The bad submission returns 400 with a list of problems, while the valid one returns a welcome message — exactly the guardrails real signups need.

3 Post/Redirect/Get and Flash Messages

After a successful POST, redirect to another page instead of returning HTML. This is the Post/Redirect/Get pattern: it stops the browser from re-submitting the form if the user refreshes.

Pair it with flash() to show a one-time confirmation message. Because flash stores the message in the session, your app needs a secret_key. The example below redirects home after a successful post and flashes a thank-you.

# app.py — Post/Redirect/Get with a flash message
from flask import Flask, request, redirect, url_for, flash, get_flashed_messages

app = Flask(__name__)
app.secret_key = "change-me-in-production"   # required for flash/sessions

@app.route("/")
def home():
    msgs = get_flashed_messages()
    banner = ("<p>" + msgs[0] + "</p>") if msgs else ""
    return banner + '<form method="POST" action="/save"><input name="title"><button>Save</button></form>'

@app.route("/save", methods=["POST"])
def save():
    title = request.form.get("title", "untitled")
    flash(f"Saved '{title}' successfully!")
    # Redirect instead of returning HTML -> refresh won't re-post
    return redirect(url_for("home"))

# Submit, follow the redirect, and see the flashed message:
with app.test_client() as client:
    resp = client.post("/save", data={"title": "Notes"}, follow_redirects=True)
    print(resp.data.decode())   # ...Saved 'Notes' successfully!...

# ✅ Expected output:
# <p>Saved 'Notes' successfully!</p><form method="POST" action="/save"><input name="title"><button>Save</button></form>

🔬 Form Handling Cheat Sheet

StepCode
Accept the submissionmethods=["GET", "POST"]
Read a fieldrequest.form.get("name")
Detect a submitrequest.method == "POST"
Redirect after saveredirect(url_for("home"))
Flash a messageflash("Saved!")

🎯 Your Turn: Fill in the Blanks

Complete the login route below. Replace each ___ so it accepts POST, reads the username, and redirects to the dashboard.

# 🎯 YOUR TURN: replace each ___ to finish the login route
from flask import Flask, request, redirect, url_for

app = Flask(__name__)
app.secret_key = "dev-key"

@app.route("/login", methods=["GET", "___"])      # allow POST too
def login():
    if request.___ == "POST":                      # which attribute is the method?
        username = request.___.get("username", "") # read the posted field
        if username:
            return ___(url_for("dashboard"))        # redirect on success
        return "Username required", 400
    return '<form method="POST"><input name="username"><button>Log in</button></form>'

@app.route("/dashboard")
def dashboard():
    return "Welcome to your dashboard!"

with app.test_client() as client:
    r = client.post("/login", data={"username": "ada"}, follow_redirects=True)
    print(r.data.decode())

Common Errors & Quick Reference

❌ The form submits but request.form is empty

Your inputs are missing the name attribute, or the form uses GET. Add name="..." to each input and set method="POST" on the form.

❌ RuntimeError: The session is unavailable / no secret key

flash and sessions need a signing key. Set app.secret_key = "..." once when you create the app.

TaskCode
Read a field safelyrequest.form.get("email", "")
Redirect after POSTreturn redirect(url_for("home"))
Enable flashapp.secret_key = "..."
Run the appflask run

🏆 Mini Challenge: A Feedback Form

Build a feedback form that validates input and confirms the submission.

# 🏆 Mini Challenge: a feedback form
from flask import Flask, request

app = Flask(__name__)

@app.route("/feedback", methods=["GET", "POST"])
def feedback():
    if request.method == "POST":
        message = request.form.get("message", "").strip()
        if not message:
            return "Message cannot be empty.", 400
        return f"Thanks for your feedback: {message}"
    return '<form method="POST"><textarea name="message"></textarea><button>Send</button></form>'

with app.test_client() as client:
    print(client.post("/feedback", data={"message": ""}).status_code)        # 400
    ok = client.post("/feedback", data={"message": "Great course!"})
    print(ok.data.decode())                                                   # Thanks for your feedback: Great course!

❓ Frequently Asked Questions

Lesson complete — you can collect real user input!

You built a form, read it through request.form, validated the data, and used Post/Redirect/Get with flash to confirm submissions safely.

Practice quiz

Where do submitted POST form values land in Flask?

  • request.args
  • request.form
  • request.json
  • request.cookies

Answer: request.form. Values from a POSTed HTML form are read from request.form.

What becomes the key for a field in request.form?

  • The input's id attribute
  • The input's type attribute
  • The input's name attribute
  • The input's class attribute

Answer: The input's name attribute. Each input's name attribute becomes a key in request.form.

How do you read a field safely without crashing if it is missing?

  • request.form["x"]
  • request.form.read("x")
  • request.form.field("x")
  • request.form.get("x")

Answer: request.form.get("x"). request.form.get("x") returns None instead of raising when the field is absent.

Which decorator argument lets one route accept both GET and POST?

  • methods=["GET", "POST"]
  • verbs="GET,POST"
  • allow="POST"
  • accept=["POST"]

Answer: methods=["GET", "POST"]. methods=["GET", "POST"] tells @app.route to accept both verbs.

How do you detect that a form was submitted (not just shown)?

  • request.is_post
  • request.method == "POST"
  • request.submitted
  • request.posted()

Answer: request.method == "POST". Checking request.method == "POST" distinguishes a submit from a GET render.

What does the Post/Redirect/Get pattern prevent?

  • SQL injection
  • Cross-site scripting
  • Duplicate form submissions on refresh
  • Slow page loads

Answer: Duplicate form submissions on refresh. Redirecting after a POST stops a browser refresh from re-submitting the form.

Which function shows a one-time confirmation message?

  • notify()
  • alert()
  • toast()
  • flash()

Answer: flash(). flash() stores a one-time message retrieved with get_flashed_messages().

Why does flash() require app.secret_key to be set?

  • Because flash stores messages in the session, which is signed
  • Because it encrypts the database
  • Because it disables debug mode
  • Because it sets the port

Answer: Because flash stores messages in the session, which is signed. flash messages live in the session, which Flask signs using secret_key.

After a successful POST, what should the save route return?

  • A 500 error
  • A redirect to another page
  • Raw form HTML again
  • Nothing at all

Answer: A redirect to another page. Returning redirect(url_for(...)) implements Post/Redirect/Get.

What is a sensible status code to return when validation fails?

  • 200 OK
  • 301 Moved
  • 404 Not Found
  • 400 Bad Request

Answer: 400 Bad Request. A failed validation typically returns 400 Bad Request with the errors.

Continue this course

Frequently asked questions

How does Flask receive HTML form data?

When a form is submitted with method="POST", the browser sends the field values in the request body. Flask exposes them through request.form, a dictionary-like object keyed by each input's name attribute.

Why use the same route for showing and processing a form?

It keeps related logic together. On a GET request you render the empty form; on a POST you read request.form, validate it, and act. Branching on request.method inside one view is the classic Flask pattern.

What is the Post/Redirect/Get pattern?

After a successful POST you redirect to another page instead of returning HTML directly. This stops the browser from re-submitting the form if the user refreshes, preventing duplicate submissions. Use redirect(url_for('route_name')).

How do I show a confirmation message after a form submits?

Use Flask's flash() to store a one-time message, then display it in the template after redirecting. It requires a secret_key because flash stores the message in the session.