SemaTrain Logo Ein Fachportal von SemaTrain

Webentwicklung: Flask oder Django (Überblick)

Sie verstehen, wann Flask (leichtgewichtig) oder Django (Full-Stack) passt – und wie Sie Routing, Templates und JSON-APIs sauber umsetzen.

Hinweis: Das Kapitel ist bewusst ein Überblick. Ziel ist ein solides Mentalmodell, damit Sie danach schnell produktiv werden.

Python Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur Python Schulung. Termine & Buchung laufen über SemaTrain.de.

Ziel: Sie können ein kleines Webprojekt einordnen/aufsetzen (Routing, Templates, JSON) und entscheiden, ob Flask oder Django besser passt.

Merksatz: Flask = „Baukasten“ (viel Freiheit), Django = „Bausatz“ (viel drin, schneller standardisiert).

Worum geht’s?

Lehr-/Lernziele

Nach diesem Kapitel können Sie …

Flask vs. Django – Entscheidungshilfe

Flask (leichtgewichtig)

  • Ideal für kleine Services, APIs, Prototypen
  • Sie wählen Komponenten selbst (ORM, Auth, Admin …)
  • Sehr gut zum Lernen von HTTP/Requests

Typisch: Microservice „Kurs-API“ oder kleines internes Tool.

Django (Full-Stack)

  • Ideal für größere Apps mit Auth, Admin, ORM, Forms
  • Viele Standards „out of the box“
  • Schnell produktiv in Team/Projekt

Typisch: Web-App „Schulungsverwaltung“ (User, Rollen, Admin).

Flask-Minimalbeispiel: Seite + JSON-API

SemaTrain-Beispiel: eine kleine Kursliste als HTML und als API.

Flask: Routing + HTML + JSON (LZ2/LZ3) (Python)
from flask import Flask, jsonify, render_template, request

app = Flask(__name__)

# Beispiel-Daten (didaktisch)
kurse = [
    {"id": 1, "kursname": "Python Schulung", "format": "Online", "standort": "Berlin", "preis_euro": 1390},
    {"id": 2, "kursname": "Python Schulung", "format": "Praesenz", "standort": "Hamburg", "preis_euro": 1490},
]

@app.get("/")
def startseite():
    return "<h1>SemaTrain – Python</h1><p>/kurse oder /api/kurse</p>"

@app.get("/kurse")
def kursseite():
    # Template-Datei: templates/kurse.html
    return render_template("kurse.html", kurse=kurse)

@app.get("/api/kurse")
def api_kurse():
    # optionaler Filter: ?format=Online
    format_filter = request.args.get("format")
    daten = kurse
    if format_filter:
        daten = [k for k in kurse if k["format"] == format_filter]
    return jsonify(daten)

if __name__ == "__main__":
    app.run(debug=True)
Template (Jinja): templates/kurse.html
Jinja-Template (LZ3) (HTML)
<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8">
    <title>Kurse</title>
  </head>
  <body>
    <h1>Kurse (Beispiel)</h1>

    <ul>
      {% for kurs in kurse %}
        <li>
          <strong>{{ kurs.kursname }}</strong> –
          {{ kurs.format }} –
          {{ kurs.standort }} –
          {{ kurs.preis_euro }} €
        </li>
      {% endfor %}
    </ul>
  </body>
</html>

Django-Minimalbeispiel: Model + View + URL

Django denkt „App-orientiert“: models.py, views.py, urls.py, templates/…

Django: Model (LZ3) (Python)
# app: kurse/models.py
from django.db import models

class Kurs(models.Model):
    kursname = models.CharField(max_length=120)
    format = models.CharField(max_length=30)      # "Online" / "Praesenz"
    standort = models.CharField(max_length=60)
    dauer_tage = models.IntegerField()
    preis_euro = models.DecimalField(max_digits=8, decimal_places=2)

    def __str__(self) -> str:
        return f"{self.kursname} ({self.format})"
Django: Views + URLs + JSON (LZ2/LZ3) (Python)
# app: kurse/views.py
from django.http import JsonResponse
from django.shortcuts import render
from .models import Kurs

def kursliste(request):
    kurse = Kurs.objects.all().order_by("preis_euro")
    return render(request, "kurse/kurse.html", {"kurse": kurse})

def api_kurse(request):
    format_filter = request.GET.get("format")
    qs = Kurs.objects.all()
    if format_filter:
        qs = qs.filter(format=format_filter)

    daten = list(qs.values("id", "kursname", "format", "standort", "preis_euro"))
    return JsonResponse(daten, safe=False)

# app: kurse/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("kurse/", views.kursliste, name="kursliste"),
    path("api/kurse/", views.api_kurse, name="api_kurse"),
]
Production-Grundidee: Debug aus + WSGI/ASGI
  • (LZ4) debug=True nur lokal – in Production aus.
  • (LZ4) Webserver spricht nicht „Python direkt“, sondern startet App über WSGI/ASGI (z.B. Gunicorn/Uvicorn).
  • (LZ4) Konfiguration/Secrets nicht hardcoden (ENV-Variablen).

Praxisaufgabe (Mini)

Sie bauen eine Mini-Web-App „Kursübersicht“: HTML + JSON-Endpoint.

Beitrag zu den Lehr-/Lernzielen: LZ1 (Framework-Wahl begründen), LZ2 (Routing/JSON), LZ3 (Template), LZ4 (Debug/Prod-Grundidee).

  1. (LZ1) Entscheiden: Flask oder Django? (Ein Satz Begründung: „Warum passt es hier?“)
  2. (LZ2) Route/URL /api/kurse: gibt Kursliste als JSON aus, Filter ?format=Online.
  3. (LZ3) Route/URL /kurse: rendert Template und zeigt Kurse an.
  4. (Bonus) Detailseite /kurse/<id> oder /kurse/<int:id>.
Lösungsvorschlag (Flask) anzeigen
Lösung: Flask Mini-Web-App (LZ2/LZ3) (Python)
from flask import Flask, jsonify, render_template_string, request, abort

app = Flask(__name__)

kurse = [
    {"id": 1, "kursname": "Python Schulung", "format": "Online", "standort": "Berlin", "preis_euro": 1390},
    {"id": 2, "kursname": "Python Schulung", "format": "Praesenz", "standort": "Hamburg", "preis_euro": 1490},
]

TEMPLATE = """
<h1>Kurse (Beispiel)</h1>
<ul>
  {% for kurs in kurse %}
    <li>
      <a href="/kurse/{{ kurs.id }}">{{ kurs.kursname }}</a>
      – {{ kurs.format }} – {{ kurs.standort }} – {{ kurs.preis_euro }} €
    </li>
  {% endfor %}
</ul>
"""

DETAIL = """
<h1>{{ kurs.kursname }}</h1>
<p>Format: {{ kurs.format }}</p>
<p>Standort: {{ kurs.standort }}</p>
<p>Preis: {{ kurs.preis_euro }} €</p>
<p><a href="/kurse">zurück</a></p>
"""

@app.get("/api/kurse")
def api_kurse():
    format_filter = request.args.get("format")
    daten = kurse if not format_filter else [k for k in kurse if k["format"] == format_filter]
    return jsonify(daten)

@app.get("/kurse")
def kursliste():
    return render_template_string(TEMPLATE, kurse=kurse)

@app.get("/kurse/<int:kurs_id>")
def kurs_detail(kurs_id: int):
    kurs = next((k for k in kurse if k["id"] == kurs_id), None)
    if kurs is None:
        abort(404)
    return render_template_string(DETAIL, kurs=kurs)

if __name__ == "__main__":
    app.run(debug=True)

Kurz-Takeaways

Quiz: Webentwicklung (Flask/Django Überblick)

1. (LZ1) Welche Aussage beschreibt Django am besten?

2. (LZ2) Was ist ein typischer JSON-Endpunkt in Flask/Django?

3. (LZ3) Wozu dienen Templates in Webprojekten?

4. (LZ4) Was ist in Production zwingend anders als lokal?

Praxisaufgabe

Mini-Projekt: Kursübersicht als Web-App (HTML + JSON-API)

Sie bauen eine kleine Web-App im SemaTrain-Kontext (Beispieldaten): Kursliste als HTML-Seite und als JSON-API. Bonus: Kurs neu anlegen per POST (API).

Beitrag zu den Lehr-/Lernzielen:
LZ1 (Framework-Wahl), LZ2 (Routing + JSON), LZ3 (Template), LZ4 (Debug/Production-Grundideen + Config/Secrets).

Aufgabe

Lösung anzeigen (Flask – kompakt)
Lösung: Flask HTML + JSON + Filter + POST + ENV-Config (Python)
from __future__ import annotations

import os
from flask import Flask, jsonify, request, abort, render_template_string

# ===== LZ4: Config/ENV (Debug nur lokal) =====
APP_ENV = os.getenv("APP_ENV", "dev")  # dev / prod
SECRET_KEY = os.getenv("SECRET_KEY", "dev-only-not-for-production")

app = Flask(__name__)
app.config["SECRET_KEY"] = SECRET_KEY

DEBUG = (APP_ENV == "dev")

# ===== Beispieldaten (didaktisch, nicht verbindlich) =====
kurse = [
    {"id": 1, "kursname": "Python Schulung", "format": "Online",   "standort": "Berlin",  "dauer_tage": 3, "preis_euro": 1390},
    {"id": 2, "kursname": "Python Schulung", "format": "Praesenz", "standort": "Hamburg", "dauer_tage": 3, "preis_euro": 1490},
    {"id": 3, "kursname": "Python Schulung", "format": "Praesenz", "standort": "Muenchen","dauer_tage": 4, "preis_euro": 1890},
]

def naechste_id() -> int:
    return (max((k["id"] for k in kurse), default=0) + 1)

def validiere_kurs(payload: dict) -> tuple[bool, str]:
    required = ["kursname", "format", "standort", "dauer_tage", "preis_euro"]
    for r in required:
        if r not in payload:
            return False, f"Feld fehlt: {r}"

    if payload["format"] not in ("Online", "Praesenz"):
        return False, "format muss Online oder Praesenz sein"

    try:
        dauer = int(payload["dauer_tage"])
        preis = float(payload["preis_euro"])
    except (TypeError, ValueError):
        return False, "dauer_tage muss int sein, preis_euro muss Zahl sein"

    if dauer <= 0:
        return False, "dauer_tage muss > 0 sein"
    if preis <= 0:
        return False, "preis_euro muss > 0 sein"

    return True, ""

# ===== LZ2: Routing/JSON =====
@app.get("/")
def index():
    return "<h1>SemaTrain – Kursübersicht</h1><p>Siehe <a href='/kurse'>/kurse</a> oder <a href='/api/kurse'>/api/kurse</a></p>"

@app.get("/api/kurse")
def api_kurse():
    format_filter = request.args.get("format")
    standort_filter = request.args.get("standort")

    daten = kurse
    if format_filter:
        daten = [k for k in daten if k["format"] == format_filter]
    if standort_filter:
        daten = [k for k in daten if k["standort"] == standort_filter]

    return jsonify(daten)

@app.get("/api/kurse/<int:kurs_id>")
def api_kurs_detail(kurs_id: int):
    kurs = next((k for k in kurse if k["id"] == kurs_id), None)
    if kurs is None:
        abort(404)
    return jsonify(kurs)

# Bonus LZ2: POST (Create)
@app.post("/api/kurse")
def api_kurs_anlegen():
    payload = request.get_json(silent=True) or {}
    ok, msg = validiere_kurs(payload)
    if not ok:
        return jsonify({"ok": False, "error": msg}), 400

    kurs = {
        "id": naechste_id(),
        "kursname": payload["kursname"],
        "format": payload["format"],
        "standort": payload["standort"],
        "dauer_tage": int(payload["dauer_tage"]),
        "preis_euro": float(payload["preis_euro"]),
    }
    kurse.append(kurs)
    return jsonify({"ok": True, "kurs": kurs}), 201

# ===== LZ3: Template/Rendering (Jinja) =====
TEMPLATE = """
<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8">
    <title>Kurse (Beispiel)</title>
  </head>
  <body>
    <h1>Kurse (Beispiel)</h1>
    <p>
      Filter:
      <a href="/kurse">alle</a> |
      <a href="/kurse?format=Online">Online</a> |
      <a href="/kurse?format=Praesenz">Praesenz</a>
    </p>

    <ul>
      {% for kurs in kurse %}
        <li>
          <strong>{{ kurs.kursname }}</strong> –
          {{ kurs.format }} –
          {{ kurs.standort }} –
          {{ kurs.dauer_tage }} Tage –
          {{ "%.2f"|format(kurs.preis_euro) }} €
          (API: <code>/api/kurse/{{ kurs.id }}</code>)
        </li>
      {% endfor %}
    </ul>

    <hr>
    <h2>API testen (Beispiel)</h2>
    <pre>
GET  /api/kurse?format=Online
GET  /api/kurse/1

POST /api/kurse
JSON:
{
  "kursname":"Python Schulung",
  "format":"Online",
  "standort":"Koeln",
  "dauer_tage":2,
  "preis_euro":990
}
    </pre>
  </body>
</html>
"""

@app.get("/kurse")
def kursseite():
    format_filter = request.args.get("format")
    daten = kurse if not format_filter else [k for k in kurse if k["format"] == format_filter]
    return render_template_string(TEMPLATE, kurse=daten)

# ===== Start (nur lokal) =====
if __name__ == "__main__":
    # LZ4: debug nur im dev
    app.run(debug=DEBUG)
Bonus: Django-Variante (kurzer Leitfaden)
  • Model Kurs in models.py (wie im Kapitel) + Migrationen.
  • HTML: View rendert Template kurse/kurse.html.
  • JSON: View gibt JsonResponse(list(qs.values(...)), safe=False) zurück.
  • POST: In Django optional über Forms/DRF – im Lernpfad reicht ein einfacher JSON-View mit Validierung.

Django lohnt sich besonders, sobald Auth, Admin, ORM, Permissions und größere Strukturen gebraucht werden.