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
- How an HTML form sends data to a Flask route
- Reading submitted values from request.form
- Using one route for both GET and POST
- Validating input before acting on it
- The Post/Redirect/Get pattern to prevent re-submits
- Showing confirmation messages with flash
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
| Step | Code |
|---|---|
| Accept the submission | methods=["GET", "POST"] |
| Read a field | request.form.get("name") |
| Detect a submit | request.method == "POST" |
| Redirect after save | redirect(url_for("home")) |
| Flash a message | flash("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.
| Task | Code |
|---|---|
| Read a field safely | request.form.get("email", "") |
| Redirect after POST | return redirect(url_for("home")) |
| Enable flash | app.secret_key = "..." |
| Run the app | flask run |
🏆 Mini Challenge: A Feedback Form
Build a feedback form that validates input and confirms the submission.
- Accept GET (show form) and POST (process it)
- Require a non-empty message field
- Return a thank-you that echoes the message, or a 400 if it's blank
# 🏆 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
- Previous: Request & Response Objects
- Next: Sessions & Cookies — Remember users across requests with signed sessions and cookies
- Quick reference: Flask cheat sheet › Forms & Sessions
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.