Was ist Flask?
Flask ist ein minimales Web-Framework für Python. Es bietet dir ein Application-Objekt, eine Möglichkeit, URLs auf Funktionen zu mappen sowie ein Request- und Response-Modell. Alles andere – Datenbanken, Authentifizierung, Formulare, Background-Jobs – ist eine Entscheidung, die du später triffst, meist durch das Hinzufügen einer Extension.
Genau diese Schlankheit ist der entscheidende Punkt. Django hat eine feste Meinung dazu, wie ein Projekt strukturiert sein sollte; Flask hat fast keine. Für eine kleine API, ein internes Tool oder das erste Web-Projekt ist diese Freiheit ein Feature. Du kannst das gesamte Framework an einem Nachmittag lesen und jede einzelne Zeile deiner eigenen App verstehen.
Flask basiert auf zwei bewährten Bibliotheken: Werkzeug für das WSGI-Plumbing und Jinja2 für Templates. Beide sind ausgereift und gut dokumentiert. Wenn man weiß, dass sie im Hintergrund laufen, lässt sich das meiste Verhalten von Flask leicht erklären.
Die kleinste Flask-App
Eine vollständige Flask-Anwendung passt in nur wenige Zeilen.
# app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "Hello, world"
if __name__ == "__main__":
app.run(debug=True)
Flask(__name__) erstellt die Anwendung und gibt an, wo Templates und statische Dateien zu finden sind. Der Decorator bindet eine URL an die darunterliegende Funktion. app.run() startet den Development-Server. Der if __name__ == "__main__"-Guard verhindert, dass der Server gestartet wird, wenn das Modul von einem Test oder einem WSGI-Server importiert wird.
Routing mit Decorators
Der @app.route Decorator ist das Herzstück von Flask. Er registriert eine URL-Regel und die Funktion, die darauf antwortet.
@app.route("/posts")
def list_posts():
return "All posts"
@app.route("/posts/<int:post_id>")
def get_post(post_id):
return f"Post {post_id}"
Regeln können variable Teile enthalten, die in spitzen Klammern geschrieben werden. Flask matcht die URL, konvertiert den Wert und übergibt ihn als Keyword-Argument an die Funktion. Wenn keine Regel matcht, gibt Flask automatisch einen 404-Fehler zurück.
URL-Variablen und HTTP-Methoden
Converter machen Variablen typsicher, bevor Ihr Code ausgeführt wird. Die gängigsten sind string (der Standard), int, float, path (welcher Slashes erlaubt) und uuid.
@app.get("/users/<username>")
def profile(username):
return f"Profile for {username}"
@app.get("/files/<path:filepath>")
def serve_file(filepath):
return f"File at {filepath}"
Flask 2.0 hat Shortcuts für jede Methode hinzugefügt. @app.get, @app.post, @app.put, @app.patch und @app.delete sind übersichtlicher, als eine methods-Liste zu übergeben, und eine einzige Funktion kann auf mehrere Methoden antworten.
@app.route("/posts", methods=["GET", "POST"])
def posts():
if request.method == "POST":
return create_post()
return list_posts()
Das Request-Objekt
Eingehende Daten befinden sich im globalen request-Objekt, das Flask für den aktuellen Request befüllt. Da es sich um einen Proxy handelt, ist es sicher, dieses einmalig am Anfang des Moduls zu importieren.
from flask import request
@app.get("/search")
def search():
query = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
return {"query": query, "page": page}
@app.post("/posts")
def create_post():
payload = request.get_json()
title = payload["title"]
return {"title": title}, 201
request.args enthält Query-String-Werte, request.form enthält Formular-Posts, request.get_json() parst einen JSON-Body, request.files enthält Uploads und request.headers liefert die rohen Header. Da es sich hierbei um Multi-Value-Mappings handelt, sollten Sie .get() verwenden und Standardwerte angeben, anstatt blind über Indizes zuzugreifen.
Antworten und JSON
Ein View kann einen String, ein Dictionary (das Flask in JSON serialisiert), ein Tuple aus (body, status) oder (body, status, headers) oder ein vollständiges Response-Objekt zurückgeben.
from flask import jsonify, make_response
@app.post("/posts")
def create_post():
post = {"id": 1, "title": "Hello"}
return jsonify(post), 201
@app.get("/ping")
def ping():
response = make_response({"pong": True})
response.headers["Cache-Control"] = "no-store"
return response
jsonify ist der explizite Weg, um JSON zurückzugeben: Es serialisiert die Daten und setzt den Content-Type. Die Rückgabe eines Dictionarys funktioniert ebenfalls, aber jsonify macht die Absicht deutlicher und unterstützt mehr Datentypen.
Templates mit Jinja2
render_template lädt eine Datei aus dem templates/-Ordner und rendert sie mit den übergebenen Daten. Jinja2 escaped Variablen standardmäßig, was vor XSS schützt.
from flask import render_template
@app.get("/posts/<int:post_id>")
def get_post(post_id):
post = {"id": post_id, "title": "Hello", "body": "First post"}
return render_template("post.html", post=post)
<!-- templates/post.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ post.title }}</title>
</head>
<body>
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
</body>
</html>
Jinja unterstützt {% extends %} für Vererbung, {% include %} für Fragmente, {% for %} und {% if %} für die Logik sowie Filter wie {{ value|upper }}. Halten Sie Templates rein präsentationsorientiert; wenn die Logik zu komplex wird, verschieben Sie diese in den View.
Statische Dateien
Standardmäßig stellt Flask alles im Ordner static/ unter /static/... bereit. Referenziere diesen mit url_for, wodurch die URL aus dem Namen des Endpunkts generiert wird.
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
Überlasse in der Production-Umgebung das Ausliefern statischer Dateien einem Reverse Proxy oder einem CDN anstelle des Python-Prozesses. Das ist performanter und entlastet die App, sodass sie sich auf die Verarbeitung von Requests konzentrieren kann.
Blueprints
Ein Blueprint ist eine Sammlung von Routes und Templates, die in einer App registriert werden können. So wächst eine Flask-App aus einem einzelnen Modul zu einem Projekt heran, ohne im Chaos zu enden.
# blog/views.py
from flask import Blueprint, jsonify
bp = Blueprint("blog", __name__, url_prefix="/blog")
@bp.get("/")
def index():
return jsonify(posts=[])
@bp.get("/<int:post_id>")
def detail(post_id):
return jsonify(id=post_id)
# app.py
from blog.views import bp as blog_bp
app.register_blueprint(blog_bp)
Blueprints besitzen ihr eigenes URL-Präfix, eigene Templates und statische Dateien. Zudem kann ein Blueprint mehrfach mit unterschiedlichen Präfixen registriert werden. Sie erleichtern zudem das Testen und den Code-Review, da jedes Feature in sich abgeschlossen ist.
Die Application Factory
Sobald Sie Blueprints verwenden, ist der nächste logische Schritt eine Factory-Funktion, die die App aufbaut. Dies ist die Standardstruktur für alles, was über eine Demo hinausgeht.
# app.py
from flask import Flask
from blog.views import bp as blog_bp
def create_app(config_object="config.Config"):
app = Flask(__name__)
app.config.from_object(config_object)
app.register_blueprint(blog_bp)
return app
# wsgi.py
from app import create_app
app = create_app()
Die Factory macht Tests trivial: Erstellen Sie eine App mit einer Test-Konfiguration, führen Sie den Client aus und verwerfen Sie die Instanz anschließend wieder. Zudem wird verhindert, dass eine halb konfigurierte globale App importiert wird – was die Hauptursache für die meisten Circular-Import-Probleme in Flask ist.
Extensions
Da der Kern minimal gehalten ist, wird der Rest durch das Ökosystem bereitgestellt. Ein paar Extensions decken die meisten Projekte ab:
- Flask-SQLAlchemy integriert das ORM von SQLAlchemy und verwaltet die Session pro Request.
- Flask-Migrate kapselt Alembic, sodass Schema-Änderungen versioniert werden.
- Flask-Login verwaltet User-Sessions und den
current_userProxy. - Flask-WTF fügt Formulare, CSRF-Schutz und Datei-Uploads hinzu.
- Flask-CORS steuert Cross-Origin-Requests für APIs.
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
# app.py
from extensions import db, migrate
def create_app():
app = Flask(__name__)
app.config.from_object("config.Config")
db.init_app(app)
migrate.init_app(app, db)
return app
Das init_app Pattern ermöglicht es, eine Extension ohne App zu erstellen und sie später zu binden. Das hält die Factory sauber und macht die Extension für Models importierbar.
Konfiguration
Die Konfiguration ist ein einfaches Mapping auf app.config. Laden Sie diese aus einer Klasse, einer Datei oder der Umgebung und halten Sie Secrets aus der Versionsverwaltung fern.
# config.py
import os
class Config:
SECRET_KEY = os.environ["SECRET_KEY"]
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
JSON_SORT_KEYS = False
app.config.from_object("config.Config")
app.config.from_prefixed_env()
from_prefixed_env() liest FLASK_* Umgebungsvariablen aus, was in Containern sehr praktisch ist. Verwenden Sie pro Umgebung unterschiedliche Klassen oder Dateien und committen Sie niemals den echten SECRET_KEY — dieser signiert Sessions und CSRF-Tokens.
Fehlerbehandlung
Registrieren Sie Error Handler, um app-weit konsistente Antworten zurückzugeben. Handler können global definiert oder auf einen Blueprint beschränkt werden.
from flask import jsonify
from werkzeug.exceptions import HTTPException
@app.errorhandler(404)
def not_found(error):
return jsonify(error="not_found"), 404
@app.errorhandler(Exception)
def unhandled(error):
if isinstance(error, HTTPException):
return error
app.logger.exception(error)
return jsonify(error="internal_error"), 500
Der Handler für HTTPException ist besonders wichtig: Die integrierten Fehler von Flask sind ebenfalls Exceptions. Ein allgemeiner Exception-Handler würde daher einen 404-Fehler in einen 500-Fehler verwandeln.
Testing
Der Test-Client von Flask führt Anfragen in-process aus, ohne dass ein Server oder ein Netzwerk benötigt wird.
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("config.TestConfig")
with app.test_client() as client:
yield client
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.get_json() == {"status": "ok"}
def test_missing_post(client):
response = client.get("/posts/999")
assert response.status_code == 404
Die Factory und test_client() arbeiten zusammen: Jeder Test erhält eine frische App, sodass keine Zustände zwischen den Tests übertragen werden können. Überschreibe die Konfiguration für Tests und verwende eine In-Memory-Datenbank, sobald du eine hinzufügst.
WSGI und Deployment
Flask ist eine WSGI-Anwendung, was bedeutet, dass jeder WSGI-Server sie ausführen kann. Der Development-Server ist ausschließlich für die Entwicklung gedacht.
pip install gunicorn
gunicorn --workers 4 --bind 0.0.0.0:8000 "app:create_app()"
Nutzen Sie in der Produktion Gunicorn oder uWSGI hinter Nginx, terminieren Sie TLS am Proxy und stellen Sie statische Dateien über den Proxy oder ein CDN bereit. Setzen Sie DEBUG=False, konfigurieren Sie das Logging und lesen Sie Secrets aus der Umgebung aus. Die Containerisierung der App macht das Deployment über verschiedene Hosts hinweg portabel.
Best Practices
- Beginnen Sie mit einem einzelnen Modul und wechseln Sie zu einer Factory und Blueprints, sobald die Anwendung wächst.
- Verwenden Sie
url_forzum Erstellen von URLs, anstatt Pfade hart zu kodieren. - Lagern Sie Konfigurationen in Klassen oder Umgebungsvariablen aus, niemals direkt im Code.
- Nutzen Sie
jsonifyund explizite Status-Codes für API-Antworten. - Validieren Sie jede Eingabe, bevor sie Ihre Geschäftslogik erreicht.
- Registrieren Sie Error-Handler, damit Fehler in einem konsistenten Format zurückgegeben werden.
- Verwenden Sie den Test-Client und eine frische App-Instanz pro Test.
- Lassen Sie den Development-Server niemals in der Production-Umgebung laufen.
Häufige Fehler
debug=Truein der Production aktiviert zu lassen, wodurch ein interaktiver Debugger exponiert wird.- Einen globalen
appüberall zu importieren und dadurch zirkuläre Imports zu erzeugen. request.jsonzu lesen, ohne fehlerhafte oder fehlende Bodys zu behandeln.- URLs in Templates hart zu codieren, anstatt
url_forzu verwenden. - Eine monolithische
app.pymit hunderten von Routes aufzubauen. - Die CSRF-Protection bei der Verwendung von Formularen zu vergessen.
- Secrets in
config.pyzu speichern und diese zu committen. - Davon auszugehen, dass Flask ein integriertes ORM oder Auth-System besitzt, und dann beides schlecht selbst zu implementieren.
Wie geht es weiter?
Flask ist der schnellste Weg, um zu verstehen, was ein Web-Framework eigentlich macht, da kaum etwas im Hintergrund verborgen bleibt. Wenn du ein ORM, Migrationen, Auth und ein Admin-Panel suchst, ohne diese selbst zusammenstellen zu müssen, lies den Django-Guide. Wenn du automatische Validierung und API-Dokumentationen basierend auf Type Hints möchtest, wechsle zu FastAPI. Um die Endpunkte, die du gerade erstellt hast, professionell zu gestalten, ist der REST APIs-Guide die ideale Ergänzung. Falls du eine Datenbank hinzufügst, deckt die Backend-Roadmap SQL ab – die Sprache, die unter dem ORM liegt.