Handling forms with Post/Redirect/Get

Flask · Practical · 12 min

Forms are how users change data, and the PRG pattern avoids duplicate submissions.

A typical form view answers two methods: GET shows the empty form, POST processes it.

The flow for a POST:

1. Read fields from request.form with .get() and strip them. 2. Validate. If there are errors, re-render the form with the errors and the user's input, and return status 400 or 422 so tests and tools see the failure. 3. On success, save, flash() a confirmation and redirect to another page.

The final redirect is the Post/Redirect/Get pattern. Without it, refreshing the result page makes the browser resubmit the POST and you get duplicate orders.

flash(message, category) stores a message in the session for the next request; the template shows them with get_flashed_messages(with_categories=True). Flashing requires SECRET_KEY because it uses the session.

Hand-written validation is fine for two fields. For real forms most projects use Flask-WTF (WTForms integration), which gives form classes with validators, renders fields and, importantly, adds CSRF protection: a hidden token proving the POST came from your own page. Flask itself has no CSRF protection, so a plain form like the example is vulnerable to cross-site request forgery in production.

For file uploads, set enctype="multipart/form-data" on the form, read request.files["photo"], and cap upload size with MAX_CONTENT_LENGTH.

Example

from flask import Flask, flash, get_flashed_messages, redirect, request, url_for

app = Flask(__name__)
app.secret_key = "dev-only"
SIGNUPS = []

@app.route("/signup", methods=["GET", "POST"])
def signup():
    errors = {}
    if request.method == "POST":
        email = request.form.get("email", "").strip()
        if "@" not in email:
            errors["email"] = "Enter a valid email."
        else:
            SIGNUPS.append(email)
            flash("Thanks for signing up!", "success")
            return redirect(url_for("done"))
    return {"form": "signup", "errors": errors}, 422 if errors else 200
@app.get("/done")
def done():
    return {"messages": get_flashed_messages()}
c = app.test_client()
print(c.post("/signup", data={"email": "nope"}).status_code)          # 422
print(c.post("/signup", data={"email": "a@b.c"}, follow_redirects=True).json)
# {'messages': ['Thanks for signing up!']}

Remember

  • GET shows the form, POST validates and either re-renders or redirects
  • Redirect after success so refresh doesn't resubmit
  • flash() needs a secret key; Flask-WTF adds validation and CSRF tokens

Watch out for

  • Shipping HTML forms without CSRF protection; Flask doesn't add it for you.

Check yourself

  1. Why redirect after successfully handling a form POST?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: flask.palletsprojects.com/en/stable/patterns/flashing/