Flask 1 - Inleiding

Auteur

Fabrice Devaux

Publicatiedatum

16 december 2025

Inleiding

Deze cursus geeft je een inleiding tot Flask, een zeer populair zogenaamd “web application framework” voor Python.

Voordat we verder ingaan op Flask zelf bekijken we eerst een aantal basis-concepten.

Browsers en Webservers

Vraag

Wat gebeurt er als je een website in je browser opent?

Antwoord
  • Client (browser) -> Server
  • HTTP
    • Request, URL, Method
    • Response, Status code
    • Headers, Payload
  • Response data:
    • HTML (inhoud)
    • CSS (opmaak)
    • Javascript (interactie)
    • Andere bestanden (beelden, videos, …)

Het antwoord (response) van de webserver is

  • Statisch, altijd hetzelfde, of
  • Dynamisch, bevat data uit externe bronnen zoals een databank of een web API

https://wizardzines.com/comics/anatomy-http-request/

https://wizardzines.com/comics/anatomy-http-request/

https://wizardzines.com/comics/anatomy-http-response/

https://wizardzines.com/comics/anatomy-http-response/

Een eigen webapplicatie server in Python

Om de toegevoegde waarde van een framework als Flask in te zien is het interessant om eerst te proberen zelf een web applicatie te maken met enkel de Python standard library.

Als eerste stap moeten we natuurlijk een server hebben waar we HTTP requests naartoe kunnen sturen. Dat kan alvast erg eenvoudig als volgt:

from http.server import HTTPServer, SimpleHTTPRequestHandler

1httpd = HTTPServer(("", 8001), SimpleHTTPRequestHandler)
httpd.serve_forever()
1
HTTPServer zal HTTP requests ontvangen op de aangegeven poort (8001) en zal SimpleHTTPRequestHandler om de request te behandelen. M.a.w. SimpleHTTPRequestHandler bepaalt wat er wordt teruggestuurd.

http://127.0.0.1:8001

Pagina’s met content

Een web applicatie moet natuurlijk iets inhoudelijks terugsturen naar de browser. Er zullen typisch ook een aantal verschillende pagina’s bestaan, bereikbaar via verschillende “paden”.

from http.server import BaseHTTPRequestHandler, HTTPServer

1class MyHandler(BaseHTTPRequestHandler):
2    def do_GET(self):
3        if self.path == "/":
4            self.send_response(200)
5            self.send_header("Content-type", "text/html")
            self.end_headers()
6            self.wfile.write("<h1>Welcome Home</h1>".encode("utf-8"))
        elif self.path == "/about":
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write("<h1>About Page</h1>".encode("utf-8"))
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write("404 Not Found".encode("utf-8"))

httpd = HTTPServer(("", 8001), MyHandler)
httpd.serve_forever()
1
We maken een eigen HTTPRequestHandler om de SimpleHTTPRequestHandler te vervangen. Hiervoor moeten we BaseHTTPRequestHandler subclassen.
2
De do_GET functie zal worden aangeroepen om GET requests af te handelen.
3
We gebruiken een aantal attributen en functies uit de BaseHTTPRequestHandler base class. Met self.path kunnen we het pad uit de URL bekijken om op die manier te bepalen wat we willen terugsturen.
4
Met self.send_response bepalen we the HTTP status code van de respons.
5
Met self.send_header kunnen we (verschillende) HTTP headers in de respons instellen.
6
Uiteindelijk kunnen we me self.wfile.write de inhoud (body) van de respons wegschrijven als een byte stream (vandaar dat .encode() nodig is).

Queries en Dynamische HTML

Het moet ook mogelijk zijn respons dynamisch te maken, bvb. op basis van query parameters in the URL.

import urllib
from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
1        parsed = urllib.parse.urlparse(self.path)
        path = parsed.path
        params = urllib.parse.parse_qs(parsed.query)

        if path == "/hello":
2            name = params.get("name", ["World"])[0]
            content = f"<h1>Hello, {name}!</h1>".encode()
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(content)


httpd = HTTPServer(("", 8001), MyHandler)
httpd.serve_forever()
1
Met urllib.parse.urlparse kan het URL pad ontleed worden, zodat we in parsed.query de verschillende query parameters overhouden. Die kunnen op hun beurt ontleed worden met urllib.parse.parse_qs zodat we uiteindelijk een dictionary krijgen met de verschillende query parameters.
2
Uit die dictionary halen we dan een parameter met de naam name om uiteindelijk de waarde ervan te gebruiken in de respons body.

http://127.0.0.1:8001/hello?name=Syntra

Templates

Als we een volledige HTML pagina, met variable onderdelen, willen terugsturen moeten we een soort templating (sjabloon) systeem toevoegen.

from http.server import HTTPServer, BaseHTTPRequestHandler

TEMPLATE = """
<html>
<head><title>{title}</title></head>
<body>
    <h1>{heading}</h1>
    <p>{body}</p>
</body>
</html>
"""

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
1            content = TEMPLATE.format(
                title="Home",
                heading="Welcome!",
                body="This is the home page.",
            )
        elif self.path == "/about":
            content = TEMPLATE.format(
                title="About",
                heading="About Us",
                body="We are learning Python web dev.",
            )
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b"404 Not Found")


httpd = HTTPServer(("", 8001), MyHandler)
httpd.serve_forever()
1
Met format kunnen waarden worden toegekend aan de variabelen in de template. Op die manier kan afhankelijk van het URL pad een verschillende pagina worden teruggestuurd, zonder de volledige inhoud telkens te herhalen in de code.

Conclusie

We merken al snel een groot aantal pijnpunten:

  • Elk URL/pad koppelen met bepaalde inhoud is ingewikkeld.
  • Handmatig template systeem uitwerken.
  • Informatie over sessies, request en response elementen moet je zelf beheren.
  • Veel ‘extra’ werk, denk bvb. aan configuratie, authenticatie, database toegang, e.d.

Dit zijn typisch zaken waarvoor je een zogenaamd Web Application Framework zou willen gebruiken. Flask is zo een framework.

Flask Basics

Wat is Flask

Van https://flask.palletsprojects.com:

Flask is a lightweight WSGI web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications.

WSGI is een standaard interface tussen Python web applicaties en web servers. Flask is dus de applicatie. Voor een productie omgeving heb je dan ook een WSGI-capabele webserver nodig. We komen hier later op terug want om te starten is dit niet belangrijk. WSGI is uitgebreid gedefinieerd in PEP 3333 – Python Web Server Gateway Interface.

Meer over WSGI: https://www.fullstackpython.com/wsgi-servers.html.

Installatie

Flask bestaat uit een package. Je gebruikt best een package manager als uv of conda om een aparte Python omgeving aan te maken om in te werken. Voor deze cursus gebruikt we Flask versie 3.

Met uv:

mkdir flask_les1
cd flask_les1
uv init
uv add "Flask~=3.0"

Of met conda:

conda create -n flask
conda activate flask
conda install "Flask~=3.0"

Terminologie

Basistermen die veel gebruikt worden:

  • App(lication): Centraal (singleton) Flask object.
  • Route: Een URL pad (bvb /home) gekoppeld aan een Python functie.
  • View: De functie voor een route. Returnt de gewenste respons.
  • Request: Object dat een ontvangen HTTP request voorstelt.
  • Response: Object dat een uit te sturen HTTP response voorstelt.

Hello World in Flask

hello.py
1from flask import Flask

2app = Flask(__name__)       # App


4@app.route("/hello")        # Route
3def hello():                # View
5    return "Hello, Syntra!"
1
De Flask class uit de flask module wordt geïmporteerd. Om te beginnen heb je niets anders nodig.
2
Het Flask object, dat de volledige (WSGI) web applicatie voorstelt. Typisch wordt de naam app gebruikt. Er is maar één verplicht argument, de package naam van de applicatie.
3
Een view functie. Deze bepaalt wat er gebeurt en teruggestuurd wordt voor een bepaalde route. De naam van de functie is vrij te kiezen maar stemt doorgaans overeen met het pad in the gekoppelde route.
4
Een route is een decorator, waardoor deze gekoppeld wordt aan een bepaalde view functie. Het enige verplichte argument is het pad.
5
De return waarde van de view functie bepaalt wat terug wordt gestuurd naar de browser. Hier een eenvoudige str maar later komen andere mogelijkheden aan bod.

Uitvoeren

De Flask Python package installeert ook een flask commando. Hiermee kan je o.a. een Flask applicatie toegankelijk maken via een eenvoudige WSGI webserver. Deze is niet geschikt voor productie toepassingen maar wel perfect om je applicatie te testen tijdens de ontwikkeling.

uv run flask --app hello run
Opmerking

Vanaf nu wordt dit commando niet meer in detail herhaald. Als je uv gebruikt moet je dus uv run flask gebruiken i.p.v. gewoon flask.

conda activate flask
flask --app hello run
Opmerking

Vanaf nu wordt dit commando niet meer in detail herhaald. Als je conda gebruikt moet je dus op voorhand het conda environment geactiveerd hebben met conda activate flask.

Het --app argument is de naam van de module of package van de Flask applicatie.

Eenmaal opgestart is de server bereikbaar via http://127.0.0.1:5000. We hebben maar 1 route, /hello, dus moeten we http://127.0.0.1:5000/hello gebruiken.

Variabele Route met URL Pad

Het vorige voorbeeld gebruikt een statische route (/hello). In het volgend voorbeeld definiëren we een dynamische route. Hierbij is een deel van het pad vrij te kiezen (variabel) en krijgt de view functie toegang via een functie argument.

hello_who.py
from flask import Flask

app = Flask(__name__)


@app.route("/hello/<name>")
def hello(name):
    return f"Hello {name}!"

Voorbeeld URL: http://127.0.0.1:5000/hello/Syntra

Denk even terug het we dit eerder “handmatig” moesten aanpakken en hoeveel eenvoudiger en duidelijk dit nu is m.b.v. Flask!

Vragen
  • Wat is het data type van name?
  • Waarom lukt het niet op type(name) te returnen? (Tip: bekijk page source.)

We kunnen ook een specifiek data type forceren met een zogenaamde converter. Bijvoorbeeld als we een int verwachten:

@app.route("/hello/<int:number>")

Op https://flask.palletsprojects.com/en/stable/quickstart/#variable-rules vind je een overzicht van de mogelijke converters.

Vragen
  • Wat gebeurt er als type niet klopt (bvb int)?
  • Is het mogelijk om twee maal dezelfde route te hebben, waarbij enkel de converter verschilt?

URL Query Parameters

Wat zijn URL Query Parameters?

URL Query Parameters zijn stukjes extra informatie die je aan een URL toevoegt na een vraagteken (?). Ze bestaan uit key-value paren.

Voorbeeld: /search?term=python&page=2

Ze worden meestal gebruikt om filters, zoektermen, sortering of paginering mee te geven aan de server. Je kunt meerdere parameters combineren met een &.

https://wizardzines.com/comics/how-urls-work/

https://wizardzines.com/comics/how-urls-work/

Code

In een view functie kan je toegang krijgen tot URL query parameters via flask.request. Dit is een speciaal object waar we later verder op ingaan. request.args is een dictionary met alle URL query parameters.

query_power.py
from flask import Flask, request

app = Flask(__name__)


@app.route("/power")
def power():
    base = request.args.get("base")
    return str(pow(int(base), 2))

Voorbeeld URL: http://127.0.0.1:5000/power?base=3

Vragen
  • Maak de exponent (nu 2) van de pow functie ook parametreerbaar.
  • Wat gebeurt er als we geen parameter(s) meegeven in the URL?
    • Hoe kunnen we dit oplossen?

Voorkomen of genezen.

Het probleem hierboven kan op twee manieren (“stijlen”) worden opgelost:

1 - LBYL
def power():
    base = request.args.get("base")
    if base:
        return str(pow(int(base), 2))
    else:
        "Missing required query parameter: base", 400

Er wordt eerst gecheckt of base (al dan niet) None is.

Deze aanpak heet “Look Before You Leap”. Er wordt eerste gekeken of de volgende stap al dan niet uitgevoerd kan worden.

2 - EAFP
def power():
    base = request.args.get("base")
    try
        return str(pow(int(base), 2))
    except TypeError:
        "Missing required query parameter: base", 400

De potentieel problematische code wordt gewoon uitgevoerd (in een try block). Probleemgevallen worden in een (of meerdere) except blokken afgehandeld.

Deze aanpaak heet “Easier to Ask Forgiveness than Permission”.

Er is geen “juiste” stijl. In Python is EAFP doorgaans populairder, mede omdat deze iets explicieter en duidelijker kan zijn. Het try/except mechanisme is in Python ook heel efficiënt.

Een mogelijke vuistregel is om te gaan kijken naar het normale/verwachte pad in the code. Is de probleemsituatie uitzonderlijk, gebruik dan EAFP. Als we kijken naar bovenstaand voorbeeld, dan kunnen we er van uitgaan dat typisch base gedefiniëerd zal zijn, en we dus eerder EAFP zullen gebruiken.

HTML Forms

Een derde manier om data van de browser naar de server/applicatie te versturen is d.m.v. een HTML Form.

<form method="post">
  <label for="username">Username</label>
  <input name="username" id="username" required>
  <label for="password">Password</label>
  <input type="password" name="password" id="password" required>
  <input type="submit" value="Log In">
</form>

HTML pagina met een Form

In eerste instantie moet de gebruiker de in te vullen form te zien krijgen. Pas daarna kan de ingevulde data verstuurt worden.

form_login.py
from flask import Flask

app = Flask(__name__)

@app.route("/login")
def login():
    return """<!DOCTYPE html>
<html>
<body>
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Log In">
  </form>
</body>
</html>
"""
Vragen
  • Wat gebeurd er als we op de Login knop klikken?
  • Welke HTTP status code stuurt de server terug?
  • Welke HTTP method wordt gebruikt om form data naar de server te versturen?

https://wizardzines.com/comics/status-codes/

https://wizardzines.com/comics/status-codes/

2 methodes, 1 route

We hebben een route (/login) waarvoor we twee methodes (GET en POST) moeten ondersteunen.

  • GET: stuur een HTML pagina met de form
  • POST: lees de form data uit
form_login_post.py
from flask import Flask, request

app = Flask(__name__)

1@app.route("/login", methods=["GET", "POST"])
def login():
2    if request.method == "POST":
3        print(f"POST username = {request.form["username"]}")
        return "Logged In!"

    return """<!DOCTYPE html>
<html>
<body>
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Log In">
  </form>
</body>
</html>
"""
1
De POST methode wordt toegelaten met het methods argument in de route decorator (default is methods=["GET"]).
2
flask.request wordt opnieuw gebruikt. Met request.method weten we wat de HTTP method is.
3
Met request.form krijgen we een dictionary met alle form data als key-value paren
Vragen
  • Op welke andere manier, zonder request.method te gebruiken, zou je dit kunnen implementeren?

Redirects en view URLs

De login pagina en code uit het vorig voorbeeld is niet erg realistisch…

  • De username en password moeten natuurlijk gevalideerd worden. Hier komen we later op terug.
  • Na een succevolle login kom je in principe terecht op de home pagina van de website.

Met een HTTP Redirect geeft de server de browser een doorverwijzing naar een andere URL. Redirects gebruiken HTTP status codes tussen 300 en 399. Op https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections vind je informatie over de verschillende soorten redirects. In ons geval, na een login, wordt in principe een HTTP 302 code gebruikt.

https://wizardzines.com/comics/http-redirects/

https://wizardzines.com/comics/http-redirects/

Met de flask.redirect functie wordt een redirect response gemaakt. Het functie argument is een string met de gewenste URL. Zo zal return redirect("https://www.google.com") de browser doorsturen naar Google.

Om naar de home pagina van de applicatie door te sturen zouden we natuurlijk een vast adres kunnen gebruiken, bvb http://127.0.0.1:5000/home. We komen snel in de problemen want het adres blijft natuurlijk niet altijd 127.0.0.1. Later veranderen we misschien ook het pad van de home pagina.

Met flask.url_for kunnen we automatisch de juiste URL genereren voor een bepaalde view functie. Het argument is een string met de naam van de functie.

login_redirect.py
from flask import Flask, redirect, request, url_for

app = Flask(__name__)

@app.route("/")
def home():
    return """<!DOCTYPE html>
<body>
  <h1>Welcome Home</h1>
  <p>(Sanitarium)</p>
</body>
</html>
"""

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        print(f"POST username = {request.form["username"]}")
        return redirect(url_for("home"))

    return """<!DOCTYPE html>
<html>
<body>
  <form method="post">
    <label for="username">Username</label>
    <input name="username" id="username" required>
    <label for="password">Password</label>
    <input type="password" name="password" id="password" required>
    <input type="submit" value="Log In">
  </form>
</body>
</html>
"""

Externe data is onveilig

Als ontwikkelaar moet je er altijd van uitgaan dat “externe” data nooit te vertrouwen is. Externe data komt bvb. van gebruikers (query parameters, forms, …) of een bron (bvb. een database) die je niet zelf beheert.

In het volgende voorbeeld hebben we twee views die hetzelfde doen, een query parameter gebruiken in de respons. Wat kan er mislopen bij de onveilige versie?

input_safe_unsafe.py
from flask import Flask, request
from markupsafe import escape

app = Flask(__name__)


@app.route("/unsafe")
def unsafe():
    name = request.args.get("name", "nameless")
    return f"Hello, {name}!"


@app.route("/safe")
def safe():
    name = request.args.get("name", "nameless")
    return f"Hello, {escape(name)}!"

Een voorbeeld van een ongewenst resultaat:

Flask Contexten en Speciale Objecten

De Request Context

The request context keeps track of the request-level data during a request. Rather than passing the request object to each function that runs during a request, the request and session proxies are accessed instead.

Bron: https://flask.palletsprojects.com/en/stable/reqcontext/

De flask.request proxy

Het request object is een proxy naar een instance van Request.

from flask import request

    name = request.args.get("name", "nameless")

Met proxy wordt hier geen HTTP proxy bedoeld zoals je misschien al hebt gebruikt.

Proxy is een zogenaamd design pattern. In de module Python Architectuur komen verschillende design patronen aan bod.

Het proxy pattern waarbij een proxy class/object een vereenvoudigde toegang biedt tot iets anders. In dit geval dus toegang tot het Request object dat bij de huidige request hoort.

De flask.session proxy

Het session object is een proxy voor de Session.

Het doet zich voor als een gewoon dictionary en laat toe tijdens een request informatie te bewaren als een browser cookie. In latere requests vanaf dezelfde browser kan diezelfde informatie terug opgehaald worden.

https://wizardzines.com/comics/cookies/

https://wizardzines.com/comics/cookies/

We bekijken een eenvoudig voorbeeldje dat een voorkeur theme (donker of licht) van de gebruiker opslaat en voor elke volgende request gebruikt.

theme_cookie.py
from flask import Flask, redirect, request, session, url_for

app = Flask(__name__)
1app.secret_key = "dev_secret_key"

2COLORS = {
    "dark": {"bg": "#333", "text": "#fff"},
    "light": {"bg": "#fff", "text": "#000"},
}


@app.route("/")
def index():
3    theme = session.get("theme", "light")
    return f"""
    <html>
        <body style="background-color:{COLORS[theme]["bg"]}; color:{COLORS[theme]["text"]};">
            <p>Current theme: <strong>{theme}</strong></p>
            <form method="POST" action="{url_for("set_theme")}">
                <button type="submit" name="theme" value="light">Switch to Light</button>
                <button type="submit" name="theme" value="dark">Switch to Dark</button>
            </form>
        </body>
    </html>
    """


@app.route("/set_theme", methods=["POST"])
def set_theme():
4    session["theme"] = request.form["theme"]
    return redirect(url_for("index"))


if __name__ == "__main__":
    app.run(debug=True)
1
Cookies worden “getekend” met een sleutel
2
De theme bepaald de kleur van de tekst en de achtergrond
3
Als er een theme key in de session dict zit gebruiken we deze
4
Met een POST request kan de gebruiker zijn voorkeur theme aanpassen en wordt deze opgeslagen via het session object

De Application Context

The application context keeps track of the application-level data during a request, CLI command, or other activity. Rather than passing the application around to each function, the current_app and g proxies are accessed instead.

Bron: https://flask.palletsprojects.com/en/stable/appcontext//

De flask.current_app proxy

Om toegang te krijgen tot het Flask applicatie-object (app) kan de flask.current_app proxy gebruikt worden. We komen hier in een volgende les op terug met een praktisch voorbeeld.

De flask.g proxy

Waar de flask.session proxy toelaat informatie op te slaan die specifiek is voor een client (browser) laat flask.g toe informatie op te slaan (en op de halen) die specifiek is voor een enkele request.

flask.request geeft toegang tot informatie in de request. flask.g geeft de mogelijkheid zelf informatie op te slaan.

Een typisch voorbeeld, waar we later nog gebruik van zullen maken, is het eenmalig aanmaken van een database sessie. Deze kan dan via flask.g overal in de code gebruikt worden, zonder dat daarvoor telkens een db object moet worden doorgegeven aan elke functie.

from flask import g

def get_db():
    if 'db' not in g:
        g.db = connect_to_database()

    return g.db

Een overzicht

Context Toepassing Voorbeeld
request Request Toegang to details van de request request.args.get("name", "nameless")
session Request Browser Cookies lezen en wegschrijven session["theme"] = request.form["theme"]
current_app Application Toegang to het app object
g Application Tijdelijke informatie voor 1 request lezen en wegschrijven g.db = connect_to_database()

Samenvatting

  • Een Web Application Framework als Flask maakt de ontwikkeling eenvoudiger d.m.v. abstracties op hoger niveau (bvb Request objecten), URL routering, parameter mapping, templates, enz
  • app = Flask(__name__) om het basis applicatie object te maken
  • Koppel een route aan een Python view functie met @app.route("/pad") decorator
  • 3 soorten input:
    • @app.route("/hello/<name>") - variabele route
    • request.args.get("name") - URL query parameter (?name=syntra)
    • request.form["username"] - HTML form
  • HTTP method
    • @app.route("/login", methods=["GET", "POST"])
    • if request.method == "POST"
  • redirect(url_for("view_functie")) - doorsturen naar URL voor view_functie
  • Speciale objecten:
    • Request Context
      • flask.request - details van de request
      • flask.session - cookies als dictionary
    • Application Context
      • flask.current_app - app object
      • flask.g - tijdelijke dictionary voor opslag per request
  • Uitvoeren met test server: flask --app module_naam run --debug

Extras

Lees/kijk-voer en referenties

Meer over WSGI