DataLens

Handling form data and query strings

Read search filters from the query string and submitted forms from the body, convert and validate them, and redisplay a form with errors.

Level: Intermediate · 10 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Requests carry user input in two main places:

Flask exposes them as request.args and request.form. Both look like dictionaries, and both contain text typed by strangers, so this module is as much about validating input as reading it.

Query strings: request.args

Customers browsing the shop want to filter: only coffee, only things under Rp 100.000. Those choices travel in the URL after a ?, like /products?category=Coffee&max_price=100000. That part is the query string, and Flask gives it to you as request.args:

from flask import Flask, request

app = Flask(__name__)
PRODUCTS = [
    {"name": "Arabica Gayo 250g", "category": "Coffee", "price": 95000},
    {"name": "Toraja Kalosi 250g", "category": "Coffee", "price": 110000},
    {"name": "Ceramic Mug", "category": "Kitchen", "price": 55000},
    {"name": "Pour-over Kettle", "category": "Kitchen", "price": 320000},
]


@app.get("/products")
def products():
    category = request.args.get("category")
    max_price = request.args.get("max_price", type=int)
    rows = PRODUCTS
    if category:
        rows = [p for p in rows if p["category"] == category]
    if max_price is not None:
        rows = [p for p in rows if p["price"] <= max_price]
    return ", ".join(p["name"] for p in rows) or "No products"


client = app.test_client()
print(client.get("/products").get_data(as_text=True))
print(client.get("/products?category=Coffee").get_data(as_text=True))
print(client.get("/products?category=Kitchen&max_price=100000").get_data(as_text=True))
print(client.get("/products?max_price=cheap").get_data(as_text=True))
Arabica Gayo 250g, Toraja Kalosi 250g, Ceramic Mug, Pour-over Kettle
Arabica Gayo 250g, Toraja Kalosi 250g
Ceramic Mug
Arabica Gayo 250g, Toraja Kalosi 250g, Ceramic Mug, Pour-over Kettle

The test client can build the query string for you: client.get("/products", query_string={"category": "Coffee"}).

Forms: request.form

Placing an order is different from filtering. It changes data, so it's sent with POST, and the values come from a form the customer filled in. Each field's name becomes the key your route reads:

<form method="post" action="/orders">
  <input name="product">
  <input name="quantity" type="number">
  <button>Order</button>
</form>
from flask import Flask, request

app = Flask(__name__)


@app.post("/orders")
def create_order():
    product = request.form.get("product", "").strip()
    quantity = request.form.get("quantity", type=int)
    return f"{quantity} × {product}"


r = app.test_client().post("/orders", data={"product": " Ceramic Mug ", "quantity": "2"})
print(r.get_data(as_text=True))
2 × Ceramic Mug

Every form value arrives as text, even from a type="number" input. Converting (type=int) and cleaning (.strip()) are your job.

Validate, then act

A robust handler collects problems first, then either reports them or acts. That's the same pattern you used for Streamlit forms and pandas validation:

from flask import Flask, request

app = Flask(__name__)
PRODUCTS = {"Ceramic Mug": 55000, "Pour-over Kettle": 320000}


def validate_order(form):
    """Return (cleaned_order, problems)."""
    problems = []
    product = form.get("product", "").strip()
    quantity = form.get("quantity", type=int)
    if product not in PRODUCTS:
        problems.append("Choose a product from the list.")
    if quantity is None or not 1 <= quantity <= 20:
        problems.append("Quantity must be a whole number from 1 to 20.")
    return {"product": product, "quantity": quantity}, problems


@app.post("/orders")
def create_order():
    order, problems = validate_order(request.form)
    if problems:
        return {"errors": problems}, 400
    total = PRODUCTS[order["product"]] * order["quantity"]
    return {"order": order, "total": total}, 201


client = app.test_client()
print(client.post("/orders", data={"product": "Ceramic Mug", "quantity": "3"}).get_json())
r = client.post("/orders", data={"product": "Teapot", "quantity": "0"})
print(r.status_code, r.get_json())
{'order': {'product': 'Ceramic Mug', 'quantity': 3}, 'total': 165000}
400 {'errors': ['Choose a product from the list.', 'Quantity must be a whole number from 1 to 20.']}

validate_order is a plain function: you can test it without Flask, and reuse it in an API and a web form.

Key idea

Never trust input. Convert it to the right type, check it against your rules, and report every problem at once with a 400. Only valid data reaches your business logic.

Redisplaying a form with errors

For HTML forms, the friendly response to bad input is the same form again, with messages and with the user's values kept:

from flask import Flask, request, render_template_string

app = Flask(__name__)

FORM = """
{% for e in errors %}<p class="error">{{ e }}</p>{% endfor %}
<form method="post">
  <input name="email" value="{{ email }}">
  <button>Subscribe</button>
</form>
"""


@app.route("/subscribe", methods=["GET", "POST"])
def subscribe():
    email = request.form.get("email", "").strip()
    errors = []
    if request.method == "POST":
        if "@" not in email:
            errors.append("That doesn't look like an email address.")
        else:
            return f"Subscribed {email}"
    return render_template_string(FORM, errors=errors, email=email), 400 if errors else 200


r = app.test_client().post("/subscribe", data={"email": "ayu.mail.id"})
print(r.status_code)
print(r.get_data(as_text=True))
400

<p class="error">That doesn&#39;t look like an email address.</p>
<form method="post">
  <input name="email" value="ayu.mail.id">
  <button>Subscribe</button>
</form>

Keeping the user's input (value="{{ email }}") means they fix one typo instead of retyping everything. And because templates escape values, even a malicious "email" can't inject HTML.

Why this matters for clean code

Input handling is where most bugs and security holes enter an application. Clean handlers have a clear shape: read → convert → validate → act, with validation in a plain function that returns problems. That keeps views short, makes the rules testable, and gives users helpful messages instead of 500 errors.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Returning JSON and building a simple API →