EthicsComputerCourses
Join training +91 90532 10052

Forms & Request Data

Django & Flask · Lesson 7 of 42 Intermediate 6 min read Updated

A website becomes useful when visitors can send data back: an order, a feedback message, a payment screenshot. In Flask all of that arrives on the request object, but where it arrives depends on how it was sent. Form fields, query strings, JSON and files each have their own place.

In this lesson you will build an order form for Pune Tiffin, read and validate its fields, show helpful error messages, redirect after a successful submit, accept JSON and file uploads safely, and see how the Flask-WTF extension adds validation classes and CSRF protection.

Where request data lives

Data sent asRead it withExample
Query string ?day=mondayrequest.argsSearch, filters, page number
HTML form with method="post"request.formOrder form, login form
JSON bodyrequest.get_json()Mobile apps, JavaScript fetch()
Uploaded filesrequest.filesPhotos, PDFs
Headers and cookiesrequest.headers, request.cookiesAPI keys, preferences

request.args and request.form behave like dictionaries. Every value is a string, even if the form field had type="number", so you always convert and check it yourself.

An order form with validation

The template shows the form, the old values (so the user does not retype everything after a mistake) and any error messages:

HTML
<!-- templates/order.html -->
<form method="post" action="{{ url_for('order') }}">
  <label>Name <input name="name" value="{{ form.get('name', '') }}"></label>
  {% if errors.name %}<p class="error">{{ errors.name }}</p>{% endif %}

  <label>Phone <input name="phone" value="{{ form.get('phone', '') }}"></label>
  {% if errors.phone %}<p class="error">{{ errors.phone }}</p>{% endif %}

  <label>Plates <input name="plates" type="number" min="1" max="20" value="{{ form.get('plates', 1) }}"></label>
  {% if errors.plates %}<p class="error">{{ errors.plates }}</p>{% endif %}

  <label><input type="checkbox" name="veg_only"> Veg only</label>
  <button>Place order</button>
</form>

The view handles both the empty form (GET) and the submitted form (POST) in one function:

Python
# app.py
from flask import Flask, redirect, render_template, request, url_for
from markupsafe import escape

app = Flask(__name__)
ORDERS = []


def validate_order(form):
    errors = {}
    name = form.get("name", "").strip()
    phone = form.get("phone", "").strip()
    plates = form.get("plates", "")

    if len(name) < 2:
        errors["name"] = "Please enter your name."
    if not (phone.isdigit() and len(phone) == 10):
        errors["phone"] = "Phone must be 10 digits."
    if not plates.isdigit() or not 1 <= int(plates) <= 20:
        errors["plates"] = "Order between 1 and 20 plates."
    return errors


@app.route("/order", methods=["GET", "POST"])
def order():
    errors = {}
    if request.method == "POST":
        errors = validate_order(request.form)
        if not errors:
            ORDERS.append({
                "name": request.form["name"].strip(),
                "phone": request.form["phone"].strip(),
                "plates": int(request.form["plates"]),
                "veg_only": "veg_only" in request.form,
            })
            return redirect(url_for("thanks", name=request.form["name"].strip()))
    return render_template("order.html", errors=errors, form=request.form)


@app.get("/thanks")
def thanks():
    name = request.args.get("name", "friend")
    return f"Thank you, {escape(name)}! Your tiffin is on its way."

Two details are worth noticing. A checkbox sends its value only when it is ticked, so "veg_only" in request.form gives True or False. And the thanks view escapes the name from the URL, because a returned string is treated as HTML.

Let us send one bad and one good submission with the test client:

Python
from app import app, ORDERS

client = app.test_client()

bad = client.post("/order", data={"name": "R", "phone": "98765", "plates": "0"})
print(bad.status)
for line in bad.get_data(as_text=True).splitlines():
    if "error" in line:
        print(line.strip())

good = client.post("/order", data={"name": "Rohan", "phone": "9876543210",
                                   "plates": "2", "veg_only": "on"})
print(good.status, good.headers["Location"])
print(ORDERS)
Output
200 OK
<p class="error">Please enter your name.</p>
<p class="error">Phone must be 10 digits.</p>
<p class="error">Order between 1 and 20 plates.</p>
302 FOUND /thanks?name=Rohan
[{'name': 'Rohan', 'phone': '9876543210', 'plates': 2, 'veg_only': True}]

The bad form is shown again with all three messages. The good form saves the order and redirects.

Post/Redirect/Get

Why redirect instead of returning "Thank you" directly from the POST? If the response to a POST is a page and the user presses Refresh, the browser offers to send the form again, and Rohan gets two tiffins. Redirecting after a successful POST makes the browser load a fresh GET page, so Refresh is harmless. This pattern is called Post/Redirect/Get (PRG), and both Flask and Django apps use it everywhere.

JSON bodies and file uploads

JavaScript and mobile apps usually send JSON. request.get_json() parses it; with silent=True it returns None instead of raising an error when the body is not valid JSON. For uploads, the form needs enctype="multipart/form-data", and files arrive in request.files.

Python
from pathlib import Path

from flask import Flask, request
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024        # 2 MB limit
UPLOAD_DIR = Path(app.root_path) / "uploads"
ALLOWED = {".jpg", ".jpeg", ".png", ".pdf"}


@app.post("/payment-proof")
def payment_proof():
    file = request.files.get("screenshot")
    if file is None or file.filename == "":
        return "Choose a file first", 400

    filename = secure_filename(file.filename)
    if Path(filename).suffix.lower() not in ALLOWED:
        return "Only JPG, PNG or PDF files", 400

    file.save(UPLOAD_DIR / filename)
    return f"Saved {filename}", 201


@app.post("/api/orders")
def api_order():
    data = request.get_json(silent=True)
    if data is None:
        return {"error": "Send JSON"}, 400
    return {"received": data, "plates": data.get("plates", 1)}, 201

Sending a file named ../../UPI receipt.png, then an .exe, then a 3 MB file, then JSON and then plain text gives:

Output
201 CREATED Saved UPI_receipt.png
400 BAD REQUEST Only JPG, PNG or PDF files
413 REQUEST ENTITY TOO LARGE
201 CREATED {'plates': 3, 'received': {'name': 'Isha', 'plates': 3}}
400 BAD REQUEST {'error': 'Send JSON'}

secure_filename() removed the ../../ that could have saved the file outside the uploads folder, and replaced the space. MAX_CONTENT_LENGTH made Flask reject the large file with 413 before your code even ran.

Flask-WTF: forms as classes

Writing validate_order() by hand teaches you what validation is, but bigger apps use the Flask-WTF extension (pip install flask-wtf). It describes each form as a class with validators, and adds CSRF protection, which plain Flask does not have. (CSRF is an attack in which another site tricks a logged-in user's browser into submitting your form; you will learn it in Security.)

Python
from flask_wtf import FlaskForm
from wtforms import BooleanField, IntegerField, StringField
from wtforms.validators import DataRequired, Length, NumberRange, Regexp

app.config["SECRET_KEY"] = "change-me"   # needed for the CSRF token


class OrderForm(FlaskForm):
    name = StringField("Name", validators=[DataRequired(), Length(min=2, max=50)])
    phone = StringField("Phone", validators=[Regexp(r"^\d{10}$", message="Phone must be 10 digits.")])
    plates = IntegerField("Plates", validators=[NumberRange(min=1, max=20)])
    veg_only = BooleanField("Veg only")


@app.route("/order", methods=["GET", "POST"])
def order():
    form = OrderForm()
    if form.validate_on_submit():          # POST and every validator passed
        ...                                # save, then redirect
    return render_template("order.html", form=form)

In the template, {{ form.csrf_token }} adds a hidden token field and {{ form.name() }} renders the input. form.name.errors holds that field's messages. Django's forms, which you will meet in Forms & ModelForms, follow the same idea and have CSRF protection built in.

Common mistakes

  • request.form["plates"] + 1. Form values are strings, so this raises a TypeError. Convert with int() after checking.
  • Forgetting method="post" on the <form>. The browser then sends a GET, and the data appears in request.args and in the address bar.
  • Missing enctype="multipart/form-data". Without it, request.files is empty.
  • Saving uploads with the user's file name. Always pass it through secure_filename() and check the extension.
  • Returning a page after a successful POST. Redirect instead (Post/Redirect/Get).

Key points

  • Query strings are in request.args, form fields in request.form, JSON in request.get_json(), files in request.files.
  • All form and query values are strings; convert and validate them on the server.
  • Re-show the form with old values and error messages when validation fails.
  • After a successful POST, redirect (Post/Redirect/Get) so a refresh does not resubmit.
  • Use secure_filename() and MAX_CONTENT_LENGTH for uploads; Flask-WTF adds form classes and CSRF protection.

Exercise

Add a /feedback page to Pune Tiffin with a form for name, rating (1 to 5) and comment (at most 200 characters). On POST, validate all three, show errors next to the fields if something is wrong, and on success store the feedback in a list and redirect to /feedback?sent=1, where the page shows "Thanks for the feedback!" above the empty form.

Show solution
Python
FEEDBACK = []


@app.route("/feedback", methods=["GET", "POST"])
def feedback():
    errors = {}
    form = request.form
    if request.method == "POST":
        name = form.get("name", "").strip()
        rating = form.get("rating", "")
        comment = form.get("comment", "").strip()
        if not name:
            errors["name"] = "Name is required."
        if rating not in {"1", "2", "3", "4", "5"}:
            errors["rating"] = "Choose a rating from 1 to 5."
        if len(comment) > 200:
            errors["comment"] = "Keep it under 200 characters."
        if not errors:
            FEEDBACK.append({"name": name, "rating": int(rating), "comment": comment})
            return redirect(url_for("feedback", sent=1))
    sent = request.args.get("sent") == "1"
    return render_template("feedback.html", errors=errors, form=form, sent=sent)
Output
POST /feedback (rating=9)        -> 200 OK, form shown with "Choose a rating from 1 to 5."
POST /feedback (valid)           -> 302 FOUND, Location: /feedback?sent=1
GET  /feedback?sent=1            -> 200 OK, "Thanks for the feedback!" shown

Checking rating against a set of allowed strings validates and limits it in one step. The sent=1 flag in the redirect URL lets the GET page show the thank-you line; the next lesson shows a cleaner way to do this with flash messages.

Quick Quiz

1. A form is sent with method="post". Where are its fields in Flask?

2. A checkbox named veg_only is not ticked. What is "veg_only" in request.form?

3. Why redirect after a successful POST?

4. What does secure_filename("../../UPI receipt.png") protect against?

Learn Django & Flask with a mentor

Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call