Handling User Input and Forms
1) Why This Matters
Websites become exciting when they interact with people. Think search boxes, sign-ups, polls, and order forms—these all collect user input. In this lesson, you'll learn how to build a simple, friendly web form and safely handle the information a user sends to your website.
2) Learning Goals
By the end, you will be able to:
- Create a basic HTML form with text boxes, number fields, selects, checkboxes, and radio buttons.
- Send form data to a Python server using Flask.
- Validate user input (make sure it's not empty, is the right type, and within allowed choices).
- Show helpful error messages and keep the user's input when something's wrong.
- Understand the difference between GET and POST methods in forms.
3) Warm-Up: Console Input vs Web Forms (2 minutes)
In basic Python, we can ask for input like this:
name = input("What is your name? ")
print("Hello,", name)On the web, users don't type into your terminal—they type into a web page form. The server (your Python app) receives that input, checks it, and replies with a new page (like a "Thanks!" screen). That's what we'll build today.
4) What We're Building
A simple "Ice Cream Order" website:
- The form asks for your name, scoop count, flavor, toppings, and more.
- The server checks the input.
- The site shows a friendly summary and total price.
It's delicious, and it teaches all the key skills you need.
5) Setup (Flask)
You'll use Flask, a small, friendly Python web framework.
- Make a new folder for your project (for example,
icecream_app). - Open your terminal or command prompt in that folder.
- Install Flask:
pip install flask - Create the files exactly like this:
Project folder structure:
icecream_app/
app.py
templates/
form.html
result.html6) HTML Form Basics (Quick Overview)
Important HTML form bits:
- form tag: wraps the whole form. Has method (GET or POST) and action (where to send).
- input elements: different types collect different data (text, number, email, checkbox, radio).
- select: a dropdown list.
- name attribute: the key used by the server to read the value (for example,
name="email"). - required, min, max: simple browser checks that help users before they submit.
7) Step-by-Step Build
A) Create app.py (the server)
Copy this code into app.py:
from flask import Flask, render_template, request
app = Flask(__name__)
ALLOWED_FLAVORS = ["Vanilla", "Chocolate", "Strawberry", "Mint"]
ALLOWED_CONES = ["Cup", "Cone"]
ALLOWED_TOPPINGS = ["Sprinkles", "Chocolate Chips", "Nuts"]
def calculate_total(scoops, toppings_count):
base = 2.50 # includes 1 scoop
if scoops > 1:
base += (scoops - 1) * 1.00
base += toppings_count * 0.50
return round(base, 2)
@app.route("/", methods=["GET", "POST"])
def order():
errors = {}
data = {
"name": "",
"email": "",
"scoops": "1",
"flavor": "Vanilla",
"toppings": [],
"cone": "Cup"
}
if request.method == "POST":
# Read form values
data["name"] = (request.form.get("name") or "").strip()
data["email"] = (request.form.get("email") or "").strip()
scoops_raw = request.form.get("scoops") or ""
data["flavor"] = request.form.get("flavor") or ""
data["toppings"] = request.form.getlist("toppings")
data["cone"] = request.form.get("cone") or ""
# Validate name
if not data["name"]:
errors["name"] = "Please enter your name."
elif len(data["name"]) > 30:
errors["name"] = "Name must be 30 characters or fewer."
# Validate email (optional)
if data["email"]:
if "@" not in data["email"] or "." not in data["email"]:
errors["email"] = "Please enter a valid email (like name@example.com)."
# Validate scoops
try:
scoops = int(scoops_raw)
if scoops < 1 or scoops > 5:
errors["scoops"] = "Scoops must be between 1 and 5."
except ValueError:
errors["scoops"] = "Scoops must be a number."
scoops = 1
data["scoops"] = str(scoops)
# Validate flavor
if data["flavor"] not in ALLOWED_FLAVORS:
errors["flavor"] = "Choose a flavor from the list."
# Validate cone
if data["cone"] not in ALLOWED_CONES:
errors["cone"] = "Choose Cup or Cone."
# Validate toppings
for t in data["toppings"]:
if t not in ALLOWED_TOPPINGS:
errors["toppings"] = "Choose toppings from the list."
break
if not errors:
total = calculate_total(scoops, len(data["toppings"]))
return render_template("result.html", data=data, total=total)
return render_template(
"form.html",
data=data,
errors=errors,
flavors=ALLOWED_FLAVORS,
cones=ALLOWED_CONES,
toppings=ALLOWED_TOPPINGS
)
if __name__ == "__main__":
app.run(debug=True)Notes:
request.formgets the data the user submitted.request.form.getlist("toppings")collects multiple checkboxes into a list.- We use simple, clear error messages so users know what to fix.
debug=Trueis helpful while learning. Don't use it for a real public website.
B) Create templates/form.html (the form page)
Copy this code into templates/form.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>PyVerse Ice Cream Order</title>
<style>
body { font-family: Arial, sans-serif; margin: 24px; }
.field { margin-bottom: 12px; }
.error { color: #b00020; font-size: 0.9em; }
label { display: block; margin-bottom: 4px; }
input[type="text"], input[type="email"], input[type="number"], select { padding: 6px; width: 260px; }
.group { margin-bottom: 10px; }
</style>
</head>
<body>
<h1>Ice Cream Order</h1>
<p>Fill out this form and we will calculate your total.</p>
<form method="post" action="/">
<div class="field">
<label for="name">Your name</label>
<input id="name" name="name" type="text" required maxlength="30" value="{{ data.name }}">
{% if errors.name %}<div class="error">{{ errors.name }}</div>{% endif %}
</div>
<div class="field">
<label for="email">Email (optional)</label>
<input id="email" name="email" type="email" value="{{ data.email }}">
{% if errors.email %}<div class="error">{{ errors.email }}</div>{% endif %}
</div>
<div class="field">
<label for="scoops">Number of scoops (1-5)</label>
<input id="scoops" name="scoops" type="number" min="1" max="5" value="{{ data.scoops }}">
{% if errors.scoops %}<div class="error">{{ errors.scoops }}</div>{% endif %}
</div>
<div class="field">
<label for="flavor">Choose a flavor</label>
<select id="flavor" name="flavor">
{% for f in flavors %}
<option value="{{ f }}" {% if data.flavor == f %}selected{% endif %}>{{ f }}</option>
{% endfor %}
</select>
{% if errors.flavor %}<div class="error">{{ errors.flavor }}</div>{% endif %}
</div>
<div class="field">
<div class="group">Toppings (choose any)</div>
{% for t in toppings %}
<label>
<input type="checkbox" name="toppings" value="{{ t }}" {% if t in data.toppings %}checked{% endif %}>
{{ t }}
</label>
{% endfor %}
{% if errors.toppings %}<div class="error">{{ errors.toppings }}</div>{% endif %}
</div>
<div class="field">
<div class="group">Cup or Cone?</div>
{% for c in cones %}
<label>
<input type="radio" name="cone" value="{{ c }}" {% if data.cone == c %}checked{% endif %}>
{{ c }}
</label>
{% endfor %}
{% if errors.cone %}<div class="error">{{ errors.cone }}</div>{% endif %}
</div>
<button type="submit">Place Order</button>
</form>
</body>
</html>C) Create templates/result.html (the results page)
Copy this code into templates/result.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Your Ice Cream Order</title>
<style>
body { font-family: Arial, sans-serif; margin: 24px; }
.summary { background: #f3f7ff; padding: 16px; border-radius: 8px; max-width: 500px; }
</style>
</head>
<body>
<h1>Thanks, {{ data.name }}!</h1>
<div class="summary">
<p>Order summary:</p>
<ul>
<li>Flavor: {{ data.flavor }}</li>
<li>Scoops: {{ data.scoops }}</li>
<li>Cup or Cone: {{ data.cone }}</li>
<li>Toppings: {% if data.toppings %}{{ data.toppings | join(", ") }}{% else %}None{% endif %}</li>
</ul>
<p>Total: ${{ "%.2f"|format(total) }}</p>
</div>
<p><a href="/">Place another order</a></p>
</body>
</html>8) Run Your App
- In your terminal, inside the project folder, run:
python app.py - Open your browser and go to:
http://127.0.0.1:5000 - Try the form. Intentionally type something wrong (like 10 scoops) to see the error. Fix it and resubmit.
9) Understanding What Just Happened
method="POST"tells the browser to send form data in a POST request.- In Flask,
request.methodlets us check if the user visited the page (GET) or submitted the form (POST). - We read values with
request.form.get("fieldname")andrequest.form.getlist("fieldname")for checkboxes. - We validate on the server because we cannot fully trust the browser. This protects your app from mistakes and bad data.
- If there are errors, we show the form again with messages and keep the user's previous input (so they don't have to retype everything).
10) GET vs POST (Simple Explanation)
- GET: Used when you're just reading data (like a search). The data appears in the URL, like
?q=chocolate. - POST: Used when sending or changing data (like submitting a form to place an order). The data is sent in the request body (not shown in the URL).
Our order form uses POST because we are creating an order and don't want the info in the URL.
11) Tips for Clear, Kind Forms
- Use labels and placeholders so users know what to type.
- Show helpful error messages near the field that has a problem.
- Keep choices limited with select, radio buttons, and checkboxes to avoid typos.
- Use min, max, and required in HTML to help users before they submit.
12) Safety and Good Habits
- Always validate on the server (like we did).
- Limit choices to allowed lists (like flavors and cone types).
- Strip spaces from inputs (we used
.strip()). - Don't rely only on browser checks—users can turn them off.
- Later, when you build bigger apps, you'll add a CSRF token (a small secret per form) to stop certain attacks. Libraries like Flask-WTF make this easy. For now, just know this is important for real projects.
13) Troubleshooting
- If you see
ModuleNotFoundError: No module named 'flask', install Flask withpip install flask. - If the page doesn't load, check the terminal for errors.
- If Jinja shows an error like
'data' is undefined, make sure you passeddata=...inrender_templateand your template uses the same names.
14) Practice Challenges
Try these improvements one by one:
- Add a new flavor and topping. Update both the allowed lists in
app.pyand the template dropdown/checkboxes. - Add a password-like field called "pickup code" and require it to be at least 4 characters.
- Add an "Allergy notes" textarea (make it optional, but limit it to 140 characters).
- Add a small discount code box. If the user types "SWEET10", take $1.00 off the total (but not below $0).
- Switch the form method to GET for a test search form and print
request.argsinstead ofrequest.formto see the difference in how data is sent.
15) Quick Quiz (Check Your Understanding)
- What is the difference between GET and POST in a form?
- Why should we validate data on the server, even if the browser checks it?
- Which Flask object lets us read submitted form values?
- How do we collect multiple checkbox values in Flask?
16) Summary
- You built a working web form with HTML and handled the data in Python using Flask.
- You learned to validate inputs, show error messages, and keep user values when something goes wrong.
- You saw the difference between GET and POST and why POST is better for private or changing data.
Great job! You now know how many real websites collect and use information. Next, we'll store those submissions in a database so your app can remember orders over time.