Learning Goals
3 min- Write an HTML form that POSTs to a route.
- Read submitted fields with
request.form. - Validate user input; redisplay the form on error.
- Follow Post/Redirect/Get with
redirect+flashmessages.
Warm-Up · One Route, Two Methods
5 minfrom flask import Flask, render_template, request app = Flask(__name__) @app.route("/contact", methods=["GET", "POST"]) def contact(): if request.method == "POST": name = request.form["name"] return f"Thanks, {name}!" return render_template("contact.html")
Same route, two HTTP methods. GET shows the form; POST processes it. Cleanest pattern in Flask.
Always redirect after a successful POST. Otherwise pressing F5 re-submits the form, leading to double-saves. This is the Post/Redirect/Get (PRG) rule — the most-broken rule on the entry-level web.
New Concept · Form, request.form, PRG
14 minThe HTML form
<form action="/contact" method="post"> <label>Name <input name="name" required></label> <label>Email <input type="email" name="email" required></label> <label>Message <textarea name="msg"></textarea></label> <button>Send</button> </form>
- Each input's
name=becomes a key inrequest.form. requiredgets free browser-side validation.action=can be the same path you're on (Flask will route by method).
Read + validate
@app.route("/contact", methods=["GET", "POST"]) def contact(): if request.method == "POST": name = request.form.get("name", "").strip() email = request.form.get("email", "").strip() msg = request.form.get("msg", "").strip() errors = [] if not name: errors.append("Name is required.") if "@" not in email: errors.append("Email looks wrong.") if errors: return render_template("contact.html", errors=errors, name=name, email=email, msg=msg) save_message(name, email, msg) flash("Message sent 🎉") return redirect(url_for("contact")) return render_template("contact.html")
flash + secret_key
app.secret_key = "change-me-in-prod" # required for flash # in template: {% with messages = get_flashed_messages() %} {% for m in messages %}<p class="flash">{{ m }}</p>{% endfor %} {% endwith %}
Re-fill form fields after error
Always pass the user's values back so they don't have to retype everything. In the template:
<input name="name" value="{{ name or '' }}" required>Worked Example · Sign-Up Form (No DB Yet)
12 min# app.py from flask import Flask, render_template, request, redirect, url_for, flash app = Flask(__name__) app.secret_key = "demo-secret" USERS = [] # in-memory list, fine for the demo @app.route("/signup", methods=["GET", "POST"]) def signup(): if request.method == "POST": name = request.form.get("name", "").strip() email = request.form.get("email", "").strip().lower() errors = [] if not name: errors.append("Name is required.") if "@" not in email: errors.append("Bad email.") if any(u["email"] == email for u in USERS): errors.append("Email already used.") if errors: return render_template("signup.html", errors=errors, name=name, email=email) USERS.append({"name": name, "email": email}) flash(f"Welcome, {name}!") return redirect(url_for("members")) return render_template("signup.html") @app.route("/members") def members(): return render_template("members.html", users=USERS)
<!-- templates/signup.html -->
{% extends "base.html" %}
{% block content %}
<h1>Sign up</h1>
{% for e in errors %}<p class="err">{{ e }}</p>{% endfor %}
<form method="post">
<label>Name <input name="name" value="{{ name or '' }}" required></label>
<label>Email <input name="email" value="{{ email or '' }}" type="email" required></label>
<button>Create account</button>
</form>
{% endblock %}
<!-- templates/members.html -->
{% extends "base.html" %}
{% block content %}
<h1>Members ({{ users|length }})</h1>
{% with msgs = get_flashed_messages() %}
{% for m in msgs %}<p class="flash">{{ m }}</p>{% endfor %}
{% endwith %}
<ul>{% for u in users %}<li>{{ u.name }} — {{ u.email }}</li>{% endfor %}</ul>
<p><a href="{{ url_for('signup') }}">+ Add another</a></p>
{% endblock %}Read the diff
Full Post/Redirect/Get: bad input redisplays the form with errors + previous values; good input redirects to the members page. Refresh after success doesn't re-add anyone. That's production-quality form handling in 40 lines.
Try It Yourself
13 minForm with one textarea. Append each comment to an in-memory list; show all below the form.
Form asks for age. Reject if < 7 or > 99 with a friendly message. Echo the bad value back into the input.
Add enctype="multipart/form-data" to the form and accept an uploaded image. Save it under uploads/.
Hint
@app.route("/upload", methods=["GET", "POST"]) def upload(): if request.method == "POST": f = request.files["photo"] if f and f.filename: f.save(f"uploads/{f.filename}") flash("uploaded") return redirect(url_for("upload")) return render_template("upload.html")
<form method="post" enctype="multipart/form-data"> <input type="file" name="photo" accept="image/*"> <button>Upload</button> </form>
Mini-Challenge · Polling Booth
8 minBuild poll.py: home page lists three options as radio buttons. After submitting, redirect to /results showing vote counts as percentages. Don't let the same browser vote twice in one session (use a cookie or session).
Recap
3 minSame route, both methods. request.form["name"] reads inputs. Validate, redisplay on error, redirect on success. Use flash + secret_key for one-shot messages. Tomorrow we plug a real database under it.
Homework
4 minAdd a contact form to your portfolio site from Lesson 39. Validate name + email + message; on success store the message in an in-memory list and show all messages on an /admin page. (Tomorrow we move from in-memory to SQLite.)