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.