LEARN PYTHON | Forms, Validation And Databases

Lesson objective

Handle GET and POST forms, validate server-side values, bind SQLite parameters and use Post/Redirect/Get after a committed save.

Learn

09.3 | FORMS, VALIDATION AND DATABASES

01 | A FORM IS A REQUEST BUILDER

An HTML form collects controls and asks the browser to submit a request. Its action chooses the destination and its method chooses how to send it. A control’s name identifies the submitted field; id mainly connects labels, styling and scripts to the element.

The learning lab’s form sends title, minutes and a hidden csrf token to /tasks/new. A field without a submitted name cannot be retrieved just because the label looks correct.

A browser is not a trusted source of business rules. Someone can send a request without using your page, so the server must still validate the data even when the HTML has required, min or maxlength.

02 | OPEN THE FORM WITH GET, SUBMIT WITH POST

The route accepts both GET and POST. GET displays the form. POST attempts validation and saving, then either shows correction feedback or redirects to the task list.

request.form contains submitted form fields. A value such as "25" arrives as text, so int converts it before the minute range is checked. Missing fields get deliberate defaults rather than crashing at an unexpected key lookup.

Use the complete learning lab download from 09.2 and follow its README. It uses fictional local data without account controls; 09.4 and the portal add identity and ownership.

HTML TEMPLATE | TEMPLATES/NEW.HTML

{% extends 'base.html' %}{% block content %}<h2>Add a task</h2>{% if error %}<p role="alert">Not saved: {{ error }}</p>{% endif %}<form method="post"><input type="hidden" name="csrf" value="{{ csrf_token() }}"><label for="title">Task title</label><input id="title" name="title" maxlength="80" value="{{ values.get('title','') }}" required><label for="minutes">Planned minutes (1-240)</label><input id="minutes" name="minutes" type="number" min="1" max="240" step="1" value="{{ values.get('minutes','25') }}" required><button type="submit">Save task</button></form>{% endblock %}

03 | A SECRET AND FORM TOKEN FOR THE LAB

The lab stores an unpredictable form token in a signed session and includes it as a hidden field. The submitted token must match before a POST reaches its route. That protection is not supplied automatically by merely writing method="post".

Set PORTAL_SECRET in the process environment before running the factory. The README gives PowerShell and macOS/Linux commands. Generate the value with secrets, keep it out of source history and use the same chosen secret for this local app across restarts.

The signing secret protects session integrity; it is not a password hash or database encryption key. Lesson 09.4 explains sessions and cross-site request forgery more fully. Do not remove the hidden field to make a failing form appear to work.

TERMINAL | GENERATE A VALUE, THEN SET IT AS THE README SHOWS

python -c "import secrets; print(secrets.token_hex(32))"
python -m flask --app app:create_app run --debug

04 | CONVERSION AND VALIDATION ANSWER DIFFERENT QUESTIONS

int("25") produces the integer 25. int("twenty-five") raises ValueError. However int("-3") succeeds, so conversion alone does not establish a useful study duration.

The lab requires a trimmed non-blank title up to eighty characters and whole minutes from one to 240. It checks those rules before opening an accepted insert operation.

If validation fails, the route returns the form with an error and preserves the submitted values for correction. The status is 400, and no new task should appear in the database.

PYTHON | VALIDATION EXCERPT FROM THE LAB

title = request.form.get("title", "").strip()
minutes = int(request.form.get("minutes", ""))
if not 1 <= len(title) <= 80:
    raise ValueError("Use 1-80 non-blank title characters.")
if not 1 <= minutes <= 240:
    raise ValueError("Minutes must be from 1 to 240.")

05 | KEEP VALUES SEPARATE FROM SQL

The insert uses ? placeholders and a separate tuple containing title and minutes. A title with an apostrophe or SQL-looking text remains a data value rather than changing the SQL instruction.

Parameters do not replace validation, access control or HTML escaping. They address query construction. The title still needs length rules when saved and escaping when displayed.

A connection is stored in g for the current application context. A teardown callback closes it after the context ends. A Python global connection shared across unrelated requests would create lifetime and concurrency problems.

PYTHON | ACCEPTED WRITE EXCERPT

db = database()
with db:
    db.execute("INSERT INTO tasks(title,minutes) VALUES(?,?)", (title, minutes))

06 | COMMIT, THEN REDIRECT

The with db block commits a successful pending transaction. If an error escapes the block, pending changes roll back. After accepted storage, the route redirects to the list with status 303.

This is Post/Redirect/Get, or PRG. The browser submits the change, follows a redirect and displays a GET result page. Refreshing that result page retrieves the list instead of normally resubmitting the original form.

PRG is not an exactly-once guarantee. Repeated clicks or network retries can still send multiple POSTs. Operations requiring strict deduplication need an explicit design such as a unique submission identifier.

StepRequest / responseMeaning
1GET /tasks/new → 200Display form and token
2POST valid values → 303Commit then redirect
3GET /tasks → 200Display accepted result
Rejected POST400 form with errorNo accepted write

07 | REPORT ACCEPTED STATE, NOT A GUESS

The task list reads the database with ORDER BY id, making its order deliberate. The JSON summary calculates task count and planned minutes from stored rows, not from a browser-only counter.

The SQLite file is placed inside the lab’s instance folder beside its source location. It is not in static and is not intended to be downloaded through an asset URL.

CREATE TABLE IF NOT EXISTS creates a missing table, but does not migrate an existing old table to a changed schema. Keep stored-data updates explicit and back up before schema experiments.

08 | TRACE A SUBMISSION DECISION

Choose a title and a minutes value. This fixed model checks the lab’s rules and shows a rejection or the accepted request flow. It makes no database write and does not provide a real session token.

09 | TEST DIRECT REQUESTS AND REJECTION

The lab includes eight test methods covering routes, static assets, valid saves, redirect flow, invalid fields, missing tokens, HTML escaping and SQL-looking input. It uses a temporary database so the real practice file stays untouched.

Testing only the browser’s numeric control misses the server boundary. Send minutes=0, minutes=241 and minutes=text directly with a valid form token, then verify the row count is unchanged.

Also test a title containing <script> as data and inspect the displayed escaped text. Database parameterisation and HTML escaping protect different boundaries and should each be tested.

TERMINAL | INSIDE THE LAB FOLDER

python -m unittest discover -s . -v

10 | EXTEND ONE RULE AT A TIME

Add a subject or completion field only after defining allowed values and expected reports. A new database column needs a deliberate migration for existing records, not just a changed CREATE statement.

This lab shows storage flow; it does not prove that submitted tasks belong to a signed-in user. In the portal, owner_id comes from the server’s loaded user, and every read or write applies its access rule.

Try it: change a valid title to include an apostrophe, submit, refresh the result, then reopen the process and confirm the accepted row remains. Record where validation, storage and rendering each happen.

PUT YOUR SKILLS TO WORK

Use the Section 9 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.

Section 9 challenges Python course navigation

MATCH THE PYTHON TERM TO ITS MEANING

Terminology

Terminology

Form

HTML controls that construct a submitted request.

Field name

The key used when a control’s value is submitted.

request.form

Flask’s collection of parsed submitted form values.

Server-side validation

Checking real submitted values inside the application.

Conversion

Changing a value’s representation or type.

Business rule

An application policy defining valid operations or values.

CSRF token

An unpredictable value checked to help reject forged state-changing requests.

Bound parameter

A data value supplied separately from SQL structure.

Transaction

Changes grouped under a commit-or-rollback decision.

Commit

Accepting a transaction’s changes into stored state.

Rollback

Undoing pending changes in a rejected transaction.

g

Flask’s context-local place for request-associated application data.

Teardown

Cleanup when the application context ends.

PRG

Post/Redirect/Get, a response flow after an accepted submission.

Idempotency

An operation property preventing repetition from changing its intended effect.

Instance folder

A local location for runtime data and configuration outside public static assets.

Migration

A deliberate transition for existing stored schema and data.

Direct request test

A test submitting values without relying on browser form controls.

Questions

Questions

CHECK YOUR UNDERSTANDING

Select all correct choices. Each exact set earns one point.

1. Which are server-side validation responsibilities?
2. Which HTML attribute identifies a submitted field key?
3. What should an invalid submission do?
4. How should SQL data values be supplied?
5. What is the normal accepted form flow here?
6. Which CSRF claims are correct?
7. Which submitted minutes values are valid for this lab?
8. What does CREATE TABLE IF NOT EXISTS not do?

EXPLAIN AND COMPARE

Write your answers before revealing each sample.

1. Explain why name and id are not interchangeable.

2. Describe validation before an insertion.

3. Explain bound parameters.

4. Explain the transaction boundary.

5. Explain Post/Redirect/Get and one limitation.

6. Explain why a browser number control is insufficient.

7. Describe a rejected-write test.

8. Distinguish CSRF and ownership checks.

CODE IT | YOUR INDEPENDENT CHALLENGE

Add a validated form with a token check and parameterised write; prove rejected fields do not create a row.

Work in your Python editor. Keep a working version, explain one design choice and record at least two tests. Use the Learn tab for worked examples before comparing your result.

Flashcards

Flashcards

Click to flip. Select the ideas you need to revisit.

0 cards selected for revision.

    Selections are kept while this page is open.

    Workbook

    Workbook

    COMING SOON

    The workbook for Forms, Validation And Databases is coming soon. Complete the matching task and written questions in your exercise book.