Skip to content

Flask Шпаргалка

Lightweight Python web framework (microframework).

01

Getting Started

First Application

Flask is a microframework — it provides the essentials and lets you add what you need. debug=True enables auto-reload and detailed error pages.

flask
# install Flask
pip install flask

# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def home():
    return render_template("index.html")

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

Running the Development Server

Never use the built-in server in production. FLASK_APP points to the module containing the app instance. Use --host=0.0.0.0 to make it reachable on the network.

flask
# Set the entry point and enable debug mode
export FLASK_APP=app.py
export FLASK_ENV=development
flask run

# Override host and port
flask run --host=0.0.0.0 --port=5000

# Or run the module directly
python app.py

Virtual Environment

A virtual environment isolates project dependencies from the system Python. requirements.txt records exact versions so others can reproduce the environment with pip install -r requirements.txt.

flask
# Create an isolated environment
python -m venv venv

# Activate (Linux/macOS)
source venv/bin/activate

# Activate (Windows PowerShell)
venv\Scripts\Activate.ps1

# Install dependencies
pip install flask
pip freeze > requirements.txt

Project Structure

Flask does not enforce a layout, but separating static files, templates, and config is conventional. The instance/ folder holds deployment-specific secrets that should not be version-controlled.

flask
myapp/
  app.py            # application factory or instance
  config.py         # configuration classes
  requirements.txt  # pinned dependencies
  instance/
    config.py       # secret instance config (not in VCS)
  static/           # CSS, JS, images
    css/style.css
  templates/        # Jinja2 templates
    base.html
    index.html
  models.py         # database models
  views.py          # route handlers

Application Factory Pattern

The factory pattern defers app creation until a function is called, which is essential for multiple instances, testing, and extensions. Flask's --app option accepts a callable that returns an app.

flask
from flask import Flask

def create_app(config_name="default"):
    app = Flask(__name__)
    app.config.from_object(f"config.{config_name.title()}Config")

    from .views import bp as views_bp
    app.register_blueprint(views_bp)

    @app.route("/health")
    def health():
        return {"status": "ok"}

    return app

# Run with: flask --app myapp create_app run
# or export FLASK_APP=myapp:create_app

Minimum Application Anatomy

Passing __name__ tells Flask where to find static files and templates. You can override folder locations with constructor arguments if your layout differs from the defaults.

flask
from flask import Flask

app = Flask(
    __name__,
    static_folder="static",
    template_folder="templates",
    static_url_path="/static",
)

# __name__ lets Flask locate resources relative to the module.
# Route decorators map URL patterns to view functions.
@app.route("/")
def index():
    return "Hello, Flask!"
02

Routing

Variable Rules

Sections marked with <converter:varname> capture URL segments. Built-in converters: string (default), int, float, path (accepts slashes), uuid. The value is passed as a function argument.

flask
from flask import Flask

app = Flask(__name__)

@app.route("/user/<username>")
def show_user(username):
    return f"User: {username}"

@app.route("/post/<int:post_id>")
def show_post(post_id):
    return f"Post #{post_id}"

@app.route("/path/<path:subpath>")
def show_path(subpath):
    return f"Path: {subpath}"

URL Converters

Converters validate and cast URL variables. Register a custom BaseConverter subclass on app.url_map.converters to enforce a regex pattern, such as product codes.

flask
@app.route("/item/<int:item_id>")
def item_int(item_id):
    return f"Integer ID: {item_id}"

@app.route("/weight/<float:kg>")
def weight(kg):
    return f"{kg} kg"

@app.route("/uid/<uuid:token>")
def by_token(token):
    return str(token)

# Custom converter
from werkzeug.routing import BaseConverter

class RegexConverter(BaseConverter):
    def __init__(self, url_map, *items):
        super().__init__(url_map)
        self.regex = items[0]

app.url_map.converters["re"] = RegexConverter

@app.route("/code/<re:[a-z]{3}-[0-9]{4}:code>")
def code(code):
    return code

Unique URLs and Redirection

Trailing slashes matter. A route ending in '/' will redirect the non-slash form; a route without '/' returns 404 for the slashed form. Pick one convention per route and stay consistent.

flask
@app.route("/projects/")
def projects():
    return "Projects page"

# /projects/  -> 200 OK
# /projects   -> 301 redirect to /projects/

@app.route("/about")
def about():
    return "About page"

# /about    -> 200 OK
# /about/   -> 404 Not Found

HTTP Methods

Use the methods argument to accept multiple verbs, or the @app.get / @app.post shortcuts (Flask 2.0+) for single-method routes. GET is the default.

flask
from flask import request, render_template

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        return do_login()
    return render_template("login.html")

# Method-specific shortcuts
@app.get("/profile")
def profile_get():
    return render_template("profile.html")

@app.post("/profile")
def profile_post():
    return update_profile()

URL Building with url_for

url_for generates URLs from the endpoint name (the view function name by default). It survives URL changes and produces absolute URLs when _external=True is passed.

flask
from flask import Flask, url_for

app = Flask(__name__)

@app.route("/")
def index():
    # builds '/user/alice' instead of hardcoding
    return f'<a href="{url_for("show_user", username="alice")}">Alice</a>'

@app.route("/user/<username>")
def show_user(username):
    return f"Hello {username}"

with app.test_request_context():
    print(url_for("show_user", username="bob"))  # /user/bob

Multiple Rules for One View

add_url_rule registers a URL without a decorator. By giving each rule a distinct endpoint but the same view function with defaults, one handler serves multiple URL shapes.

flask
from flask import Flask

app = Flask(__name__)

def show(id=None, name=None):
    if id is not None:
        return f"By id: {id}"
    return f"By name: {name}"

app.add_url_rule("/by-id/<int:id>", "by_id", show, defaults={"name": None})
app.add_url_rule("/by-name/<name>", "by_name", show, defaults={"id": None})

# Two endpoints share one function with different defaults.
03

Request and Response

The Request Object

request is a thread-local proxy bound to the current request context. It exposes the URL, method, headers, body, and client info without being passed explicitly.

flask
from flask import Flask, request

app = Flask(__name__)

@app.route("/info")
def info():
    return {
        "url": request.url,
        "method": request.method,
        "remote_addr": request.remote_addr,
        "user_agent": str(request.user_agent),
        "headers": dict(request.headers),
    }

Making a Response

make_response gives full control over status, headers, and body. For simple cases, return a string, a dict (JSON), or a tuple of (body, status, headers).

flask
from flask import Flask, make_response

app = Flask(__name__)

@app.route("/")
def index():
    resp = make_response("Hello, World!")
    resp.status_code = 201
    resp.headers["X-Custom"] = "yes"
    resp.set_cookie("visited", "1")
    return resp

# Returning a tuple: (body, status, headers)
@app.route("/quick")
def quick():
    return "OK", 202, {"X-Quick": "1"}

Response Objects

Use Response when you need a custom mimetype (e.g., XML, CSV) or headers. Setting Content-Disposition to attachment forces a download in the browser.

flask
from flask import Response

@app.route("/xml")
def xml():
    return Response("<msg>hi</msg>", mimetype="application/xml")

@app.route("/text")
def text():
    return Response("plain text", mimetype="text/plain")

@app.route("/csv")
def csv():
    body = "a,b,c\n1,2,3\n"
    headers = {"Content-Disposition": "attachment; filename=data.csv"}
    return Response(body, mimetype="text/csv", headers=headers)

Custom Status and Headers

Return a (body, status, headers) tuple for concise custom responses. Use 204 No Content for successful responses with no body, and standard HTTP status codes for semantics.

flask
@app.route("/created")
def created():
    return "done", 201, {"Location": "/resource/1"}

@app.route("/no-content")
def no_content():
    return "", 204

@app.route("/teapot")
def teapot():
    return "I'm a teapot", 418

Streaming Responses

A generator return value streams data to the client. Useful for large files (avoids loading the whole file into memory) and Server-Sent Events. Set an appropriate mimetype.

flask
from flask import Response

def generate():
    yield "data: first\n\n"
    yield "data: second\n\n"
    yield "data: third\n\n"

@app.route("/stream")
def stream():
    return Response(generate(), mimetype="text/event-stream")

@app.route("/big-file")
def big_file():
    def chunks():
        with open("large.log", "rb") as f:
            while chunk := f.read(8192):
                yield chunk
    return Response(chunks(), mimetype="application/octet-stream")

Cookies in Response

set_cookie writes a Set-Cookie header; delete_cookie expires it. Always set httponly and samesite for security. Cookies are part of the response, not the request body.

flask
from flask import make_response

@app.route("/set-theme")
def set_theme():
    resp = make_response("theme set")
    resp.set_cookie("theme", "dark", max_age=30*86400, httponly=True, samesite="Lax")
    return resp

@app.route("/clear-theme")
def clear_theme():
    resp = make_response("cleared")
    resp.delete_cookie("theme")
    return resp
04

Request Object (form/args/files)

Query Parameters (args)

request.args is a MultiDict of query string values. Use get with a default and type conversion, and getlist for repeated keys. Never index directly — missing keys raise 400.

flask
from flask import request

@app.route("/search")
def search():
    q = request.args.get("q", "")           # default ""
    page = request.args.get("page", 1, type=int)
    tags = request.args.getlist("tag")       # /search?tag=a&tag=b -> ["a","b"]
    return {"q": q, "page": page, "tags": tags}

Form Data

request.form holds POSTed application/x-www-form-urlencoded or multipart data. Use .get() to avoid KeyError on optional fields. GET requests have an empty form.

flask
from flask import request, render_template

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        if not username or not password:
            return "Missing fields", 400
        return f"Welcome {username}"
    return render_template("login.html")

File Uploads

request.files is a MultiDict of FileStorage objects. Always pass user-supplied filenames through secure_filename to strip path separators and dangerous characters before saving.

flask
from flask import request
from werkzeug.utils import secure_filename
import os

@app.route("/upload", methods=["POST"])
def upload():
    file = request.files.get("file")
    if not file or file.filename == "":
        return "No file", 400
    name = secure_filename(file.filename)
    file.save(os.path.join("uploads", name))
    return "saved"

JSON Body

get_json() parses the body as JSON only when the Content-Type is application/json. force=True ignores the header (use with caution). silent=True suppresses parse errors.

flask
from flask import request

@app.route("/api/echo", methods=["POST"])
def echo():
    data = request.get_json()        # returns None if not JSON
    if data is None:
        return "Expected JSON", 415
    return {"you_sent": data}

# Force parsing even without the right header (risky)
@app.route("/api/force")
def force():
    data = request.get_json(force=True)
    return data

Headers and Cookies

request.headers is an EnvironHeaders dict-like object; request.cookies holds incoming cookies sent by the client. Both are read-only — set response headers/cookies on the response.

flask
from flask import request

@app.route("/inspect")
def inspect():
    auth = request.headers.get("Authorization")
    accept = request.headers.get("Accept")
    session_id = request.cookies.get("session_id")
    return {
        "auth": auth,
        "accept": accept,
        "session_id": session_id,
    }

Request Methods and Properties

request exposes metadata about the connection and body. is_json checks the Content-Type, is_secure checks for HTTPS, and query_string is the raw bytes after '?'.

flask
from flask import request

@app.route("/state")
def state():
    return {
        "method": request.method,
        "is_xhr": request.is_json,
        "content_type": request.content_type,
        "content_length": request.content_length,
        "is_secure": request.is_secure,
        "scheme": request.scheme,
        "host": request.host,
        "path": request.path,
        "query_string": request.query_string.decode(),
    }
05

Jinja2 Templates

Rendering Templates

render_template loads a Jinja2 file from the templates/ folder and injects keyword arguments as variables. The {{ }} syntax outputs an expression; Flask auto-escapes HTML for safety.

flask
from flask import render_template

@app.route("/<name>")
def hello(name):
    return render_template("hello.html", name=name, title="Welcome")

# templates/hello.html
# <!DOCTYPE html>
# <html>
#   <head><title>{{ title }}</title></head>
#   <body>
#     <h1>Hello {{ name }}!</h1>
#   </body>
# </html>

Template Variables

Jinja2 supports attribute (user.name) and item (user['age']) access. The safe filter marks a string as trusted so it is not escaped — only use it on content you fully control.

flask
# View
@app.route("/")
def index():
    return render_template(
        "index.html",
        user={"name": "Alice", "age": 30},
        items=["apple", "banana", "cherry"],
        html_content="<b>safe?</b>",
    )

# Template
# <p>Name: {{ user.name }}</p>
# <p>Age: {{ user["age"] }}</p>
# <p>First: {{ items[0] }}</p>
# <p>Raw HTML: {{ html_content | safe }}</p>

Control Structures

Jinja2 supports {% if %}, {% for %}, and {% block %}. Inside loops, the special loop variable gives index, index0, first, last, length, and revindex. The for-else runs when the iterable is empty.

flask
{# if / elif / else #}
{% if user.age >= 18 %}
  Adult
{% elif user.age >= 13 %}
  Teen
{% else %}
  Child
{% endif %}

{# for loop #}
<ul>
{% for item in items %}
  <li>{{ loop.index }}: {{ item }}</li>
{% else %}
  <li>No items</li>
{% endfor %}
</ul>

Template Inheritance

extends pulls in a base template and child templates override named blocks. This keeps shared layout (navigation, scripts) in one place. super() renders the parent block's content.

flask
{# templates/base.html #}
<!DOCTYPE html>
<html>
<head>
  <title>{% block title %}Default{% endblock %}</title>
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

{# templates/child.html #}
{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
  <h1>Welcome home</h1>
{% endblock %}

Filters

Filters transform values via the pipe (|) syntax and accept arguments. Register custom filters on app.jinja_env.filters to reuse logic across templates.

flask
{{ name | capitalize }}      {# alice -> Alice #}
{{ price | round(2) }}       {# 3.14159 -> 3.14 #}
{{ items | length }}         {# count #}
{{ text | truncate(20) }}    {# shorten #}
{{ tags | join(", ") }}      {# ["a","b"] -> "a, b" #}
{{ value | default("n/a") }} {# fallback #}
{{ html | striptags }}       {# remove HTML tags #}

{# Custom filter #}
def reverse_filter(s):
    return s[::-1]

app.jinja_env.filters["reverse"] = reverse_filter
{{ "hello" | reverse }}  {# olleh #}

Macros

Macros are reusable template snippets, like functions. Import them with {% from %} or {% import %}. They keep repetitive HTML (form fields, cards) DRY.

flask
{# templates/macros.html #}
{% macro input(name, value="", type="text") %}
  <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{% macro label(text, for_id) %}
  <label for="{{ for_id }}">{{ text }}</label>
{% endmacro %}

{# In another template #}
{% from "macros.html" import input, label %}

<form>
  {{ label("Username", "u") }}
  {{ input("username") }}
  {{ label("Password", "p") }}
  {{ input("password", type="password") }}
</form>
06

Static Files

Serving Static Files

Flask automatically registers a /static/<path:filename> route for the static folder. Always build URLs with url_for so paths stay correct even if the static_url_path changes.

flask
# Files in the static/ folder are served at /static/<filename>
# static/css/style.css -> http://localhost:5000/static/css/style.css

# Reference them in templates
# <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
# <script src="{{ url_for('static', filename='js/app.js') }}"></script>
# <img src="{{ url_for('static', filename='img/logo.png') }}">

url_for for Static Assets

url_for('static', filename=...) resolves a static file path. Pass _external=True for an absolute URL (useful in emails or feeds) and query args for cache busting.

flask
from flask import Flask, url_for

app = Flask(__name__)

with app.test_request_context():
    print(url_for("static", filename="css/style.css"))
    # /static/css/style.css
    print(url_for("static", filename="js/app.js", _external=True))
    # http://localhost/static/js/app.js

Custom Static Folder

Override static_folder (disk location) and static_url_path (URL prefix) to match your project conventions. The default is folder='static' mapped to /static.

flask
from flask import Flask

app = Flask(
    __name__,
    static_folder="assets",       # folder on disk
    static_url_path="/public",    # URL prefix
)

# Now files in assets/ are served at /public/<filename>
# Template: url_for('static', filename='css/style.css') -> /public/css/style.css

Favicon

Browsers request /favicon.ico at the root by default. Add an explicit route or link the icon via a <link> tag. send_from_directory safely serves a file from a directory.

flask
from flask import send_from_directory
import os

@app.route("/favicon.ico")
def favicon():
    return send_from_directory(
        os.path.join(app.root_path, "static"),
        "favicon.ico",
        mimetype="image/vnd.microsoft.icon",
    )

# Or place favicon.ico in static/ and link it:
# <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">

Cache Busting

Browsers cache static assets by URL. Appending a version hash forces clients to fetch new copies after a file changes. A build tool (Vite, Flask-Assets) automates this in larger projects.

flask
# Append a version query param to force reloads
# <link href="{{ url_for('static', filename='css/app.css', v='1.2') }}">

# Or compute a hash at startup
import hashlib, os

def asset_version(filename):
    path = os.path.join(app.static_folder, filename)
    with open(path, "rb") as f:
        return hashlib.md5(f.read()).hexdigest()[:8]

@app.template_filter("bust")
def bust(filename):
    v = asset_version(filename)
    return url_for("static", filename=filename) + f"?v={v}"

# Template: <link href="{{ 'css/app.css' | bust }}">
07

Blueprints

Creating a Blueprint

A Blueprint groups related routes, templates, and static files into a module. The first argument is the blueprint's name; url_prefix prepends a path to every route it defines.

flask
# auth.py
from flask import Blueprint, render_template

bp = Blueprint("auth", __name__, url_prefix="/auth")

@bp.route("/login")
def login():
    return render_template("auth/login.html")

@bp.route("/register")
def register():
    return render_template("auth/register.html")

Registering Blueprints

register_blueprint attaches a blueprint to the app. You can override url_prefix at registration time, so the same blueprint can be mounted under different paths in different apps.

flask
from flask import Flask
from auth import bp as auth_bp
from blog import bp as blog_bp

app = Flask(__name__)
app.register_blueprint(auth_bp)           # /auth/*
app.register_blueprint(blog_bp, url_prefix="/blog")  # /blog/*

# Register later, or with a name override
app.register_blueprint(auth_bp, name="alt_auth")

Blueprint with URL Prefix

Define url_prefix on the Blueprint or at registration. Versioning the API path (/api/v1) lets you introduce /api/v2 later without breaking existing clients.

flask
# api/__init__.py
from flask import Blueprint

bp = Blueprint("api", __name__, url_prefix="/api/v1")

@bp.route("/users")
def list_users():
    return {"users": []}

@bp.route("/users/<int:uid>")
def get_user(uid):
    return {"id": uid}

# All routes become /api/v1/users...

Blueprint Resources

A blueprint can carry its own templates and static folders. Template resolution checks the blueprint first, then the app, so blueprint-local files override app-wide ones with the same name.

flask
bp = Blueprint(
    "admin",
    __name__,
    url_prefix="/admin",
    template_folder="templates",   # blueprint-local templates
    static_folder="static",        # blueprint-local static
)

# Templates: blueprint lookups first, then app templates
# render_template("admin/dashboard.html")
# Static: url_for('admin.static', filename='css/admin.css')

Blueprint Error Handlers

Blueprint-level error handlers only catch errors raised by that blueprint's routes, giving you section-specific error pages. App-level handlers handle anything uncaught.

flask
from flask import Blueprint, render_template

bp = Blueprint("blog", __name__)

@bp.errorhandler(404)
def blog_not_found(e):
    return render_template("blog/404.html"), 404

@bp.errorhandler(403)
def blog_forbidden(e):
    return render_template("blog/403.html"), 403

Nested Blueprints

Flask 2.0+ supports nesting blueprints. Register a child blueprint on a parent with register_blueprint; their url_prefixes concatenate, letting you compose large apps from small modules.

flask
from flask import Blueprint

parent = Blueprint("parent", __name__, url_prefix="/parent")
child = Blueprint("child", __name__, url_prefix="/child")

@child.route("/info")
def info():
    return "nested info"

parent.register_blueprint(child)
app.register_blueprint(parent)

# Final URL: /parent/child/info
08

Context (g / session)

Application Context

The application context makes current_app and g available outside a request (e.g., in CLI commands or background tasks). Push it with app.app_context() when working outside a request.

flask
from flask import current_app, g

@app.route("/")
def index():
    app_name = current_app.name
    config_value = current_app.config["SECRET_KEY"]
    return f"Running {app_name}"

# Push a context manually (scripts, shell)
with app.app_context():
    print(current_app.config["DEBUG"])

The g Object

g is a per-request namespace for storing shared resources like database connections. It is reset on each request and cleaned up in a teardown_appcontext handler.

flask
from flask import g
import sqlite3

def get_db():
    if "db" not in g:
        g.db = sqlite3.connect("app.db")
        g.db.row_factory = sqlite3.Row
    return g.db

@app.teardown_appcontext
def close_db(exc):
    db = g.pop("db", None)
    if db is not None:
        db.close()

Request Context

The request context binds request, session, and url_for to a thread. Use test_request_context in tests and scripts to simulate a request without an HTTP server.

flask
from flask import request

# Flask pushes a request context automatically during dispatch.
# Push one manually to use request/url_for outside a view:
with app.test_request_context("/?q=flask"):
    print(request.args.get("q"))   # flask
    print(request.path)            # /

# request_context for real WSGI environments
from werkzeug.test import EnvironBuilder
env = EnvironBuilder("/post/1").get_environ()
with app.request_context(env):
    print(request.path)

current_app and request Proxies

current_app, request, session, and g are LocalProxy objects that forward attribute access to the active context. Never capture them in a global variable — always access them at call time.

flask
from flask import current_app, request, session, g

# These are LocalProxy objects, not the real thing.
# They resolve to the current context's object at access time.

@app.route("/")
def index():
    current_app.logger.info("hit /")
    g.request_start = time.time()
    session["visited"] = True
    return current_app.config["APP_NAME"]

# Avoid storing proxies in module-level variables;
# always access them inside a context.

Session Object

session is a signed cookie-based dict. Set app.secret_key so Flask can sign it. Values are stored client-side and readable by the user (but not tamperable), so never store secrets there.

flask
from flask import session

@app.route("/login")
def login():
    session["user_id"] = 42
    session["role"] = "admin"
    return "logged in"

@app.route("/who")
def who():
    uid = session.get("user_id")
    return f"user {uid}" if uid else "anonymous"

@app.route("/logout")
def logout():
    session.clear()
    return "logged out"

Context Lifecycle

Understanding the lifecycle is key to using hooks correctly. teardown_* handlers always run, even on errors, so they are the right place to close database connections and release resources.

flask
# Per-request lifecycle:
# 1. Application context pushed
# 2. Request context pushed
# 3. before_request handlers run
# 4. View function runs
# 5. after_request handlers run (can modify response)
# 6. Request context popped
# 7. teardown_request handlers run
# 8. teardown_appcontext handlers run (close resources)

@app.before_request
def before():
    g.start = time.time()

@app.teardown_request
def teardown(exc):
    elapsed = time.time() - getattr(g, "start", time.time())
    app.logger.debug("request took %.3fs", elapsed)
09

SQLAlchemy Integration

Setup Flask-SQLAlchemy

Flask-SQLAlchemy wraps SQLAlchemy and integrates it with Flask's app context. Set SQLALCHEMY_DATABASE_URI to the connection string and disable track modifications to save memory.

flask
pip install flask-sqlalchemy

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

db = SQLAlchemy(app)

# In a factory, init with db.init_app(app)

Defining Models

Models subclass db.Model; columns are db.Column instances. Common types include Integer, String, Text, DateTime, Boolean. Use unique=True and nullable=False for constraints.

flask
from datetime import datetime

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def __repr__(self):
        return f"<User {self.username}>"

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    body = db.Column(db.Text)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))

Querying Data

Model.query is a convenience query interface. filter_by(keyword=value) is simple; filter(expression) supports operators like >, <, like, in_. paginate() returns items plus page metadata.

flask
# Fetch all
users = User.query.all()

# Primary key lookup
user = User.query.get(1)

# Filter
admins = User.query.filter_by(role="admin").all()
active = User.query.filter(User.email.isnot(None)).first()

# Order, limit, paginate
recent = User.query.order_by(User.created_at.desc()).limit(10).all()
page = User.query.paginate(page=2, per_page=20)

# Count
total = User.query.count()

Adding and Updating

All changes happen in a transaction via db.session. add stages a new object; commit writes it. On error, rollback with db.session.rollback() to keep the session usable.

flask
# Create
u = User(username="alice", email="[email protected]")
db.session.add(u)
db.session.commit()

# Update
u.email = "[email protected]"
db.session.commit()

# Delete
db.session.delete(u)
db.session.commit()

# Bulk insert
db.session.add_all([User(username="b"), User(username="c")])
db.session.commit()

Relationships

db.relationship defines the link between models. backref adds a reverse attribute. lazy='dynamic' returns a query object instead of a list, useful for large collections you want to filter further.

flask
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))
    posts = db.relationship("Post", backref="author", lazy="dynamic")

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))

# Usage
user = User.query.get(1)
user.posts.all()              # list of posts
post = Post.query.get(5)
post.author.username          # backref to user

Migrations with Flask-Migrate

Flask-Migrate wraps Alembic to version-control your schema. Run migrate to autogenerate a script from model changes, then upgrade to apply it. Always review autogenerated scripts before committing.

flask
pip install flask-migrate

from flask_migrate import Migrate

migrate = Migrate(app, db)

# CLI commands
# flask db init          # create migrations folder
# flask db migrate -m "create users"
# flask db upgrade       # apply migrations
# flask db downgrade     # revert one step
# flask db current       # show current revision
10

WTForms (Flask-WTF)

Installing Flask-WTF

Flask-WTF integrates WTForms with Flask and adds CSRF protection automatically. A SECRET_KEY is required so the CSRF token can be signed. Disable CSRF only for public API endpoints.

flask
pip install flask-wtf

from flask import Flask
from flask_wtf import FlaskForm

app = Flask(__name__)
app.config["SECRET_KEY"] = "change-me-in-production"
app.config["WTF_CSRF_ENABLED"] = True

# CSRF protection is enabled by default
# when SECRET_KEY is set.

Defining a Form

Forms subclass FlaskForm. Each field takes a label and a list of validators that run on validate(). Validators enforce required input, length, email format, and more.

flask
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class LoginForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
    submit = SubmitField("Log In")

class RegisterForm(FlaskForm):
    username = StringField("Username", validators=[DataRequired(), Length(min=3, max=20)])
    email = StringField("Email", validators=[DataRequired(), Email()])
    submit = SubmitField("Sign Up")

Rendering Form in Template

form.hidden_tag() renders the CSRF token and any hidden fields. Each field can render its label, input, and errors separately. For quick output, {{ form.field() }} renders the HTML input.

flask
{# templates/login.html #}
<form method="POST">
  {{ form.hidden_tag() }}  {# CSRF token #}
  <p>
    {{ form.email.label }}
    {{ form.email() }}
    {% for err in form.email.errors %}<span class="err">{{ err }}</span>{% endfor %}
  </p>
  <p>
    {{ form.password.label }}
    {{ form.password() }}
  </p>
  {{ form.submit() }}
</form>

Validating Form

validate_on_submit() returns True only for a POST request where all validators pass. On success, read field values from form.field.data. On failure, re-render the template to show errors.

flask
from flask import render_template, redirect, url_for, flash

@app.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():   # POST + valid
        email = form.email.data
        flash(f"Logged in as {email}")
        return redirect(url_for("dashboard"))
    return render_template("login.html", form=form)

# validate_on_submit() returns False on GET or invalid POST

Built-in Validators

Validators are callables that raise ValidationError on failure. EqualTo is common for password confirmation. Chain them in a list; they run in order until one fails.

flask
from wtforms.validators import (
    DataRequired,   # field not empty
    Email,          # valid email format
    Length,         # Length(min=3, max=20)
    NumberRange,    # NumberRange(min=0, max=100)
    URL,            # valid URL
    Regexp,         # Regexp(r"^\\w+$")
    EqualTo,        # EqualTo("password") - match another field
    Optional,       # skip validation if empty
    InputRequired,  # field submitted (even if empty string)
    NoneOf,         # NoneOf(["admin", "root"])
)

password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
confirm = PasswordField("Confirm", validators=[EqualTo("password")])

FileField

Use FileField from flask_wtf.file with FileRequired and FileAllowed to validate uploads. The uploaded file is a FileStorage object available at form.field.data. The form tag must include enctype=multipart/form-data.

flask
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed

class UploadForm(FlaskForm):
    image = FileField("Image", validators=[
        FileRequired(),
        FileAllowed(["jpg", "png", "gif"], "Images only!"),
    ])
    submit = SubmitField("Upload")

@app.route("/upload", methods=["GET", "POST"])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        f = form.image.data
        f.save("uploads/" + f.filename)
        return "uploaded"
    return render_template("upload.html", form=form)
11

Flask-Login Authentication

Setup Flask-Login

Flask-Login manages user sessions: logging in, logging out, and remembering users. Set login_view so unauthenticated users are redirected there. login_message flashes on that redirect.

flask
pip install flask-login

from flask import Flask
from flask_login import LoginManager

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

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
login_manager.login_message = "Please log in to access this page."

User Model

The user model must mixin UserMixin (which provides is_authenticated, is_active, get_id, etc.) and implement a user_loader callback that returns a user by ID. Never store plaintext passwords — hash them.

flask
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    password_hash = db.Column(db.String(256))

    def set_password(self, pw):
        self.password_hash = generate_password_hash(pw)

    def check_password(self, pw):
        return check_password_hash(self.password_hash, pw)

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

Login View

login_user loads the user into the session. Pass remember=True for persistent cookies. Always verify the password with a hash check before logging in, and flash a generic error on failure.

flask
from flask import render_template, redirect, url_for, flash
from flask_login import login_user
from forms import LoginForm

@app.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for("dashboard"))
        flash("Invalid username or password")
    return render_template("login.html", form=form)

Protecting Routes

The @login_required decorator rejects anonymous users and redirects them to login_view. current_user is a proxy to the logged-in user (or an AnonymousUserMixin) available in views and templates.

flask
from flask_login import login_required, current_user

@app.route("/dashboard")
@login_required
def dashboard():
    return f"Welcome {current_user.username}"

@app.route("/settings")
@login_required
def settings():
    return render_template("settings.html", user=current_user)

# Unauthenticated users redirect to login_view
# @login_required must be the innermost decorator

Logout

logout_user clears the user from the session. Protect the route with @login_required so only logged-in users can log out. Redirect to a public page (index or login) afterward.

flask
from flask_login import logout_user, login_required
from flask import redirect, url_for, flash

@app.route("/logout")
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for("index"))

# logout_user() clears the session and cookies

Remember Me and current_user

remember=True sets a separate cookie so users stay logged in after closing the browser. current_user works in both views and templates, making it easy to conditionally render UI based on auth status.

flask
from flask_login import current_user

# In the login view
login_user(user, remember=True)

# remember=True stores a separate long-lived cookie
# so the session survives a browser restart.

# current_user is available everywhere a context exists
@app.route("/")
def index():
    if current_user.is_authenticated:
        return f"Hi {current_user.username}"
    return "Welcome, guest"

# In templates:
# {% if current_user.is_authenticated %}
#   <a href="{{ url_for('logout') }}">Logout</a>
# {% endif %}
12

Error Handling

abort()

abort() immediately stops the view and returns an HTTP error. Pass a status code and optional message. It raises an HTTPException that Flask converts into the matching error response.

flask
from flask import abort

@app.route("/user/<int:id>")
def get_user(id):
    user = find_user(id)
    if user is None:
        abort(404)            # Not Found
    if not can_access(user):
        abort(403, "Forbidden")   # Forbidden with description
    return render_template("user.html", user=user)

# Common codes: 400 Bad Request, 401 Unauthorized,
# 403 Forbidden, 404 Not Found, 418 Teapot, 500 Server Error

Custom Error Pages

Register handlers with @app.errorhandler(code). Return a tuple of (template, status_code). These handlers catch both abort() calls and exceptions that map to that status.

flask
from flask import render_template

@app.errorhandler(404)
def not_found(e):
    return render_template("errors/404.html"), 404

@app.errorhandler(403)
def forbidden(e):
    return render_template("errors/403.html"), 403

@app.errorhandler(500)
def server_error(e):
    return render_template("errors/500.html"), 500

Error Handlers for Exceptions

You can register handlers for custom exception classes, not just HTTP codes. Raising the exception anywhere in a view triggers the handler, centralizing error responses for domain errors.

flask
from werkzeug.exceptions import HTTPException

class InvalidUsage(Exception):
    status_code = 400
    def __init__(self, message, status_code=None, payload=None):
        super().__init__()
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

@app.errorhandler(InvalidUsage)
def handle_invalid_usage(e):
    return {"error": e.message}, e.status_code

@app.route("/raise")
def raise_err():
    raise InvalidUsage("Bad input", status_code=422)

Catch-All 404 Handler

A generic Exception handler is a last resort for 500s. Re-raise HTTPException so built-in pages still work, and log the traceback. Be careful not to leak stack traces to users in production.

flask
@app.errorhandler(404)
def page_not_found(e):
    # Log the missing URL for analysis
    app.logger.info("404: %s", request.path)
    return render_template("errors/404.html", path=request.path), 404

@app.errorhandler(Exception)
def unhandled(e):
    # Catch any uncaught exception -> 500
    if isinstance(e, HTTPException):
        return e
    app.logger.exception("Unhandled error")
    return render_template("errors/500.html"), 500

Logging Errors

Log errors with exc_info=True to capture the full traceback. A RotatingFileHandler prevents log files from growing without bound. In production, route logs to a centralized service.

flask
import logging
from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler("app.log", maxBytes=10*1024*1024, backupCount=5)
handler.setLevel(logging.ERROR)
handler.setFormatter(logging.Formatter(
    "%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]"
))
app.logger.addHandler(handler)

@app.errorhandler(500)
def server_error(e):
    app.logger.error("Server error: %s", e, exc_info=True)
    return "Internal error", 500

HTTP Exception Handling

HTTPException is the base class for all Werkzeug HTTP errors. A handler for HTTPException catches all of them at once, while a subclass-specific handler (NotFound) takes precedence for that code.

flask
from werkzeug.exceptions import HTTPException, NotFound, BadRequest

@app.errorhandler(HTTPException)
def handle_http_exception(e):
    # All HTTP exceptions in one handler
    return render_template(
        "errors/generic.html",
        code=e.code,
        name=e.name,
        description=e.description,
    ), e.code

# Specific subclasses still work
@app.errorhandler(NotFound)
def specific_404(e):
    return "custom 404", 404
13

JSON and API

jsonify

jsonify converts Python dicts/lists to a JSON response with the correct Content-Type and ASCII-safe escaping. As of Flask 2.1+ you can pass a list directly; older versions require a dict.

flask
from flask import jsonify

@app.route("/api/user")
def api_user():
    return jsonify(username="alice", age=30)

@app.route("/api/users")
def api_users():
    users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
    return jsonify(users)

# jsonify sets Content-Type: application/json
# and dumps with proper escaping

Returning JSON

Returning a dict or list directly is the simplest way to send JSON. For custom status codes or headers, return a tuple. Use Response + json.dumps only when you need full control over serialization.

flask
# Returning a dict/list is auto-converted to JSON (Flask 1.1+)
@app.route("/api/quick")
def quick():
    return {"status": "ok", "count": 42}

# With custom status and headers
@app.route("/api/created", methods=["POST"])
def created():
    return {"id": 99}, 201, {"Location": "/api/99"}

# A Response with JSON body
from flask import Response
import json

@app.route("/api/raw")
def raw():
    return Response(json.dumps({"k": "v"}), mimetype="application/json")

Receiving JSON

get_json() parses the body as JSON when Content-Type is application/json. Always validate the parsed structure before using it — never trust client input. Return 400/422 for malformed payloads.

flask
from flask import request, jsonify

@app.route("/api/echo", methods=["POST"])
def echo():
    data = request.get_json()
    if not data:
        return jsonify({"error": "Invalid JSON"}), 400
    return jsonify({"received": data})

# Validate the structure
@app.route("/api/signup", methods=["POST"])
def signup():
    data = request.get_json()
    if not data or "email" not in data:
        return jsonify({"error": "email required"}), 422
    return jsonify({"email": data["email"]}), 201

API Blueprint

Group API routes in a blueprint with a versioned url_prefix (/api/v1). This separates API logic from page routes and makes versioning explicit — bump to /api/v2 when breaking changes land.

flask
from flask import Blueprint, jsonify, request

api = Blueprint("api", __name__, url_prefix="/api/v1")

@api.route("/health")
def health():
    return jsonify(status="ok")

@api.route("/items", methods=["GET", "POST"])
def items():
    if request.method == "POST":
        return jsonify(created=True), 201
    return jsonify(items=[])

# Register: app.register_blueprint(api)

CORS

flask-cors adds the Access-Control-Allow-Origin header so browsers permit cross-origin requests. Restrict origins to trusted domains in production rather than using the wildcard '*'.

flask
pip install flask-cors

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# Enable for all routes
CORS(app)

# Or restrict to specific origins
CORS(app, resources={r"/api/*": {"origins": ["https://example.com", "http://localhost:3000"]}})

# Per-route
@app.route("/api/public")
@cross_origin(origins="*")
def public():
    return {"data": "open"}

API Error Responses

A helper function keeps error responses uniform across endpoints — same shape, same key names. Consistent error bodies make client-side error handling far easier. Include details that help debug without leaking secrets.

flask
from flask import jsonify, abort

def api_error(message, status=400, **extra):
    body = {"error": message}
    body.update(extra)
    return jsonify(body), status

@app.route("/api/users/<int:id>")
def get_user(id):
    user = find_user(id)
    if not user:
        return api_error("User not found", 404, user_id=id)
    return jsonify(user.serialize())

# Consistent error shape across the API:
# {"error": "User not found", "user_id": 1}
14

File Upload

Basic Upload

The form tag needs enctype=multipart/form-data for file uploads. Set MAX_CONTENT_LENGTH to reject oversized uploads with a 413 before the body is fully read — this protects the server.

flask
from flask import request
import os

app.config["UPLOAD_FOLDER"] = "uploads"
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 16 MB

@app.route("/upload", methods=["POST"])
def upload():
    file = request.files["file"]
    file.save(os.path.join(app.config["UPLOAD_FOLDER"], file.filename))
    return "uploaded"

# HTML form must include enctype:
# <form method="POST" enctype="multipart/form-data">
#   <input type="file" name="file">
#   <button>Upload</button>
# </form>

Secure Filename

secure_filename strips directory traversal and unsafe characters from user-supplied filenames. It does not guarantee uniqueness — append a UUID or check for collisions if overwriting is a risk.

flask
from werkzeug.utils import secure_filename
import os

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files.get("file")
    if not f:
        return "no file", 400

    safe = secure_filename(f.filename)  # "my file!!/x.txt" -> "my_file__x.txt"
    if not safe:
        return "invalid filename", 400

    f.save(os.path.join("uploads", safe))
    return f"saved as {safe}"

Multiple Files

Use request.files.getlist('files') with an input that has the multiple attribute to accept several files at once. Validate each file's extension and size before saving.

flask
from flask import request

@app.route("/upload-many", methods=["POST"])
def upload_many():
    files = request.files.getlist("files")
    saved = []
    for f in files:
        if f and f.filename:
            name = secure_filename(f.filename)
            f.save(os.path.join("uploads", name))
            saved.append(name)
    return {"saved": saved}

# <form method="POST" enctype="multipart/form-data">
#   <input type="file" name="files" multiple>
# </form>

Upload with WTForms

Flask-WTF's FileField validates the extension and presence of the upload. The form tag must include enctype=multipart/form-data. The uploaded file is accessible at form.photo.data.

flask
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed

class PhotoForm(FlaskForm):
    photo = FileField("Photo", validators=[
        FileRequired(),
        FileAllowed(["jpg", "jpeg", "png"], "Images only"),
    ])
    submit = SubmitField("Upload")

@app.route("/photo", methods=["GET", "POST"])
def photo():
    form = PhotoForm()
    if form.validate_on_submit():
        f = form.photo.data
        f.save(os.path.join("uploads", secure_filename(f.filename)))
        return "uploaded"
    return render_template("photo.html", form=form)

Validating File Type and Size

Validate both extension and content length before saving. MAX_CONTENT_LENGTH rejects huge requests early, but per-file checks give finer control. For stronger safety, inspect the file's magic bytes, not just the extension.

flask
import os
from flask import request

ALLOWED = {".png", ".jpg", ".jpeg", ".gif", ".pdf"}
MAX_SIZE = 5 * 1024 * 1024  # 5 MB

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files.get("file")
    if not f or not f.filename:
        return "no file", 400

    ext = os.path.splitext(f.filename)[1].lower()
    if ext not in ALLOWED:
        return "file type not allowed", 415

    f.stream.seek(0, 2)        # seek to end
    size = f.stream.tell()
    f.stream.seek(0)
    if size > MAX_SIZE:
        return "file too large", 413

    f.save(os.path.join("uploads", secure_filename(f.filename)))
    return "ok"
15

Cookies and Sessions

Setting Cookies

Cookies are set on the response object via set_cookie. Set httponly=True to prevent JavaScript from reading them (XSS defense), secure=True for HTTPS-only, and samesite='Lax' or 'Strict' for CSRF defense.

flask
from flask import make_response

@app.route("/set-cookie")
def set_cookie():
    resp = make_response("cookie set")
    resp.set_cookie("user", "alice", max_age=3600, httponly=True)
    resp.set_cookie("theme", "dark", path="/", secure=True, samesite="Lax")
    return resp

# max_age in seconds; expires sets a datetime
# httponly blocks JS access; secure requires HTTPS
# samesite: "Lax" (default), "Strict", or "None"

Reading Cookies

request.cookies is a read-only ImmutableMultiDict of cookies sent by the browser. Use .get() with a default rather than indexing, since a cookie may not exist. Modifying cookies happens on the response.

flask
from flask import request

@app.route("/show-cookies")
def show_cookies():
    user = request.cookies.get("user", "guest")
    theme = request.cookies.get("theme", "light")
    all_cookies = request.cookies.to_dict()
    return f"user={user}, theme={theme}"

# request.cookies is a read-only dict
# use .get() with a default to avoid KeyError

Session Object

session is a signed cookie storing a serialized dict. Because it is signed with secret_key, users cannot tamper with it, but they CAN read it — so never store passwords or secrets in the session.

flask
from flask import session

app.secret_key = "a-very-secret-string"

@app.route("/login")
def login():
    session["user_id"] = 1
    session["role"] = "admin"
    return "logged in"

@app.route("/dashboard")
def dashboard():
    if "user_id" not in session:
        return "please log in", 401
    return f"user {session['user_id']}"

@app.route("/logout")
def logout():
    session.pop("user_id", None)
    return "logged out"

Secret Key

secret_key signs session cookies and CSRF tokens. Use a long random value, and load it from the environment so it is not committed to source control. All app instances must share the same key.

flask
import os
from flask import Flask

app = Flask(__name__)

# Generate a strong random key
app.secret_key = os.urandom(32)

# Or load from environment (recommended for production)
app.secret_key = os.environ.get("SECRET_KEY")

# For multiple workers, all must share the same key
# so sessions survive being routed to different processes.
# A key of at least 32 random bytes is recommended.

Flash Messages

flash stores a message in the session that survives a redirect and is shown on the next request. Call get_flashed_messages in the template to retrieve and clear them. with_categories lets you style by type.

flask
from flask import flash, redirect, url_for, render_template

app.secret_key = "secret"   # flash uses the session

@app.route("/save", methods=["POST"])
def save():
    flash("Saved successfully!", "success")
    return redirect(url_for("index"))

# In the template:
# {% with messages = get_flashed_messages(with_categories=true) %}
#   {% for category, message in messages %}
#     <div class="alert alert-{{ category }}">{{ message }}</div>
#   {% endfor %}
# {% endwith %}

Session Configuration

By default sessions are cookie-based and expire when the browser closes. Set session.permanent=True and PERMANENT_SESSION_LIFETIME to extend them. Flask-Session moves storage server-side (Redis, filesystem) for larger or more secure sessions.

flask
app.config.update(
    SECRET_KEY="secret",
    SESSION_COOKIE_NAME="sid",
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SECURE=True,     # HTTPS only
    SESSION_COOKIE_SAMESITE="Lax",
    PERMANENT_SESSION_LIFETIME=3600,  # seconds
    SESSION_TYPE="redis",            # needs Flask-Session
)

# Mark a session as permanent
from flask import session
from datetime import timedelta

@app.route("/remember")
def remember():
    session.permanent = True
    session["user"] = "alice"
    return "remembered for 1 hour"
16

Before/After Request Hooks

before_request

before_request handlers run before each view. Use them for auth checks, maintenance mode, or loading shared state onto g. Returning a value skips the view and uses that response directly.

flask
from flask import g, request

@app.before_request
def check_maintenance():
    if app.config.get("MAINTENANCE"):
        return "Site under maintenance", 503

@app.before_request
def load_user():
    token = request.headers.get("Authorization")
    if token:
        g.user = verify_token(token)

# Return a value to short-circuit the request

after_request

after_request handlers run after the view, receiving and returning the response. They are perfect for adding headers, logging, or caching. If an unhandled exception occurs, after_request is skipped.

flask
@app.after_request
def add_security_headers(resp):
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["X-Frame-Options"] = "SAMEORIGIN"
    return resp

@app.after_request
def log_response(resp):
    app.logger.info("%s %s -> %s", request.method, request.path, resp.status_code)
    return resp

# Must take and return a Response object

teardown_request

teardown_request runs at the end of every request, even on errors — unlike after_request. It receives the exception (or None) and is the right place to release resources like DB connections or file handles.

flask
import sqlite3
from flask import g

def get_db():
    if "db" not in g:
        g.db = sqlite3.connect("app.db")
    return g.db

@app.teardown_request
def close_db(exc):
    db = g.pop("db", None)
    if db is not None:
        db.close()

# teardown_request always runs, even if the view raised

before_first_request (deprecated)

before_first_request was removed in Flask 2.3 because it breaks with modern async servers and multi-process setups. Run one-time setup inside an app context at import time or in a CLI command.

flask
# Flask 2.3+ removed before_first_request.
# Old code:
# @app.before_first_request
# def init():
#     warm_cache()

# Modern alternative: use the app context at startup
with app.app_context():
    warm_cache()

# Or a CLI command run once during deployment
@app.cli.command("init")
def init_cmd():
    warm_cache()

Modifying Response

Multiple after_request handlers chain together in reverse registration order. Each must return a response. They are useful for security headers, caching directives, and tracing IDs.

flask
@app.after_request
def no_cache(resp):
    resp.headers["Cache-Control"] = "no-store"
    resp.headers["Pragma"] = "no-cache"
    return resp

@app.after_request
def add_request_id(resp):
    rid = getattr(g, "request_id", "n/a")
    resp.headers["X-Request-ID"] = rid
    return resp

# Multiple after_request handlers run in reverse
# registration order; each receives the previous one's output.

Database Connection Pattern

A common pattern: open a connection on demand in get_db (stored on g) and close it in teardown_appcontext. This gives each request its own connection and guarantees cleanup even on errors.

flask
import sqlite3
from flask import g, current_app

def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(
            current_app.config["DATABASE"],
            detect_types=sqlite3.PARSE_DECLTYPES,
        )
        g.db.row_factory = sqlite3.Row
    return g.db

@app.teardown_appcontext
def close_db(exc):
    db = g.pop("db", None)
    if db is not None:
        db.close()

@app.route("/users")
def users():
    db = get_db()
    rows = db.execute("SELECT * FROM users").fetchall()
    return {"users": [dict(r) for r in rows]}
17

CLI Commands

Custom Commands

Use @app.cli.command to register a CLI command, decorated with click arguments and options. The docstring becomes the help text. Run flask --help to list all available commands.

flask
import click
from flask import Flask

app = Flask(__name__)

@app.cli.command("create-user")
@click.argument("name")
@click.option("--admin", is_flag=True, default=False)
def create_user(name, admin):
    """Create a new user."""
    role = "admin" if admin else "user"
    click.echo(f"Created {name} as {role}")

# Run: flask create-user alice --admin

flask run

flask run starts the Werkzeug dev server. --debug enables reloader and interactive debugger. --app lets you point to a module or factory. Never use this server in production — it is single-threaded and insecure.

flask
# Basic
flask run

# Custom host/port
flask run --host=0.0.0.0 --port=8000

# Enable reloader and debugger
flask run --debug

# Specify the app explicitly
flask --app myapp run
flask --app myapp:create_app run

# With SSL (development)
flask run --cert=cert.pem --key=key.pem

Shell Context

shell_context_processor injects names into the flask shell so you can experiment without imports. This is invaluable for debugging models and running ad-hoc queries against your app.

flask
from flask import Flask
from models import db, User

app = Flask(__name__)

@app.shell_context_processor
def make_shell_context():
    return {"db": db, "User": User, "app": app}

# flask shell
# >>> User.query.all()
# >>> db.session.add(User(username="x"))

Application Factory with CLI

When using the factory pattern, register CLI commands inside create_app so they have access to the configured app. Flask's --app option detects the factory and invokes it for you.

flask
# myapp/__init__.py
from flask import Flask

def create_app():
    app = Flask(__name__)
    app.config.from_object("config.Config")

    @app.cli.command("init-db")
    def init_db():
        """Initialize the database."""
        click.echo("Initialized")

    return app

# Run: flask --app myapp init-db
# The factory is detected automatically.

CLI Groups

Group related commands under a parent with @app.cli.group. Subcommands are registered on the group, keeping the CLI organized: flask db init, flask db migrate, etc.

flask
import click
from flask import Flask

app = Flask(__name__)

@app.cli.group("db")
def db_group():
    """Database commands."""
    pass

@db_group.command("init")
def db_init():
    click.echo("DB initialized")

@db_group.command("drop")
def db_drop():
    click.echo("DB dropped")

# flask db init
# flask db drop

Environment Variables

FLASK_APP and FLASK_DEBUG configure the CLI. Use .flaskenv for shared dev settings and .env for secrets (both auto-loaded if python-dotenv is installed). Keep secrets out of .flaskenv since it is typically committed.

flask
# Flask reads these env vars:
# FLASK_APP      -> module/app to run (app.py, myapp, myapp:create_app)
# FLASK_DEBUG    -> 1/0 to toggle debug mode
# FLASK_ENV      -> development/production (deprecated, use FLASK_DEBUG)
# FLASK_RUN_PORT -> default port
# FLASK_RUN_HOST -> default host

# .env or .flaskenv files are auto-loaded by python-dotenv
# .flaskenv (committed): FLASK_APP=app.py
# .env (gitignored): SECRET_KEY=..., DATABASE_URL=...

# Prefer app.config.from_envvar or from_object for app secrets.
18

Testing

Test Client Basics

test_client() returns a client that makes requests without a network. Set TESTING=True for better error messages and to disable error catching. Use a fixture so each test gets a fresh app.

flask
import pytest
from myapp import create_app

@pytest.fixture()
def client():
    app = create_app()
    app.config["TESTING"] = True
    with app.test_client() as client:
        with app.app_context():
            pass  # init DB here
        yield client

def test_home(client):
    resp = client.get("/")
    assert resp.status_code == 200
    assert b"Hello" in resp.data

Making Requests in Tests

The test client mimics a browser: get/post/put/delete with data (form), json, query_string, and headers. Use resp.get_json() to parse a JSON response and resp.headers to inspect them.

flask
def test_get(client):
    resp = client.get("/users/1")
    assert resp.status_code == 200

def test_post_form(client):
    resp = client.post("/login", data={"username": "a", "password": "b"})
    assert resp.status_code == 302  # redirect

def test_post_json(client):
    resp = client.post("/api/echo", json={"key": "value"})
    assert resp.get_json() == {"received": {"key": "value"}}

def test_query(client):
    resp = client.get("/search?q=flask&page=2")
    assert resp.status_code == 200

def test_headers(client):
    resp = client.get("/", headers={"X-Test": "1"})

Testing JSON API

Pass json=... to send a JSON body with the right Content-Type. Assert on status, the parsed JSON body, and headers like Location. Testing both success and error paths ensures the API behaves consistently.

flask
def test_create_user(client):
    resp = client.post(
        "/api/users",
        json={"username": "alice", "email": "[email protected]"},
    )
    assert resp.status_code == 201
    body = resp.get_json()
    assert body["username"] == "alice"
    assert "id" in body
    assert resp.headers["Location"].endswith(str(body["id"]))

def test_error(client):
    resp = client.post("/api/users", json={})
    assert resp.status_code == 422
    assert "error" in resp.get_json()

Fixtures with pytest

Fixtures provide reusable setup: an in-memory database, a test client, and a CLI runner. Yield the resource and clean up after. test_cli_runner() invokes CLI commands in tests and checks their output and exit code.

flask
import pytest
from myapp import create_app, db

@pytest.fixture()
def app():
    app = create_app()
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture()
def client(app):
    return app.test_client()

@pytest.fixture()
def runner(app):
    return app.test_cli_runner()

def test_cli(runner):
    result = runner.invoke(args=["create-user", "alice"])
    assert result.exit_code == 0

Test Database

Use an in-memory SQLite database for fast, isolated tests. Create tables and seed data inside the fixture, then drop everything after. Each test starts with a clean slate, preventing cross-test contamination.

flask
import pytest
from myapp import create_app, db
from myapp.models import User

@pytest.fixture()
def app():
    app = create_app()
    app.config["TESTING"] = True
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
    with app.app_context():
        db.create_all()
        db.session.add(User(username="alice"))
        db.session.commit()
        yield app
        db.session.remove()
        db.drop_all()

def test_user_count(app):
    with app.app_context():
        assert User.query.count() == 1

Mocking

patch replaces external calls (HTTP, email, queues) with mocks so tests stay fast and deterministic. Assert the mock was called with expected arguments. Always patch where the name is looked up, not where it is defined.

flask
from unittest.mock import patch, MagicMock
import myapp

def test_external_call(client):
    with patch("myapp.requests.get") as mock_get:
        mock_get.return_value = MagicMock(status_code=200, json=lambda: {"ok": True})
        resp = client.get("/proxy")
        assert resp.status_code == 200
        mock_get.assert_called_once()

def test_email(client):
    with patch("myapp.send_email") as mock_send:
        client.post("/register", data={"email": "[email protected]"})
        mock_send.assert_called_once_with("[email protected]", "Welcome!")
19

Deployment

Production Server Warning

The Werkzeug dev server is single-threaded and insecure — never expose it to the internet. Use a production WSGI server (Gunicorn, uWSGI, Waitress) behind a reverse proxy. The debug debugger is a remote code execution risk.

flask
# WARNING: Flask's built-in server is for development only!
# app.run() / flask run -> single-threaded, no security, not scalable

# For production use a WSGI server:
# - Gunicorn (Linux/macOS)
# - uWSGI
# - Waitress (Windows-friendly)
# Behind a reverse proxy (Nginx, Apache, Caddy)

# The dev server also enables the interactive debugger
# when debug=True, which allows arbitrary code execution.
# NEVER deploy with debug=True.

Gunicorn

Gunicorn is the most popular WSGI server for Linux/macOS. The formula (2 × CPU + 1) workers is a good starting point. Behind Nginx, bind to 127.0.0.1 and let the proxy handle TLS and static files.

flask
pip install gunicorn

# Basic
gunicorn "app:app"

# With factory
gunicorn "myapp:create_app()"

# Workers and binding
gunicorn -w 4 -b 0.0.0.0:8000 "app:app"

# With timeout and logging
gunicorn -w 4 -b 0.0.0.0:8000 --timeout 120 --access-logfile - "app:app"

# Recommended workers: (2 * CPU) + 1
# Use --preload to share memory, or -k gevent for async.

uWSGI

uWSGI is a high-performance, highly configurable WSGI server. The master process manages workers; vacuum cleans up on exit. Pair with Nginx using the uwsgi protocol for best throughput.

flask
pip install uwsgi

# Command line
uwsgi --http 0.0.0.0:8000 --module app:app --processes 4 --threads 2

# INI config (uwsgi.ini)
# [uwsgi]
# module = app:app
# http = 0.0.0.0:8000
# processes = 4
# threads = 2
# master = true
# vacuum = true
# die-on-term = true

# Run: uwsgi uwsgi.ini

Reverse Proxy (Nginx)

Nginx sits in front of Gunicorn: it serves static files directly (faster), terminates TLS, and forwards requests to the WSGI server. The X-Forwarded-* headers let Flask see the real client IP and scheme — set ProxyFix to trust them.

flask
# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /path/to/app/static/;
        expires 30d;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}

Docker

A slim Python base image keeps the image small. Install dependencies before copying code to leverage Docker's layer cache. Pass secrets via environment variables (--env-file), never bake them into the image.

flask
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]

# .dockerignore
# __pycache__/
# venv/
# *.pyc
# .env

# Build and run
# docker build -t myapp .
# docker run -p 8000:8000 --env-file .env myapp

Environment Configuration

Layer configuration: defaults in code, environment-specific overrides, and instance secrets from env vars. This keeps secrets out of source control and lets one codebase run in dev, staging, and production.

flask
import os
from flask import Flask

app = Flask(__name__)
app.config.from_object("config.DefaultConfig")

# Override with environment-specific config
env = os.environ.get("FLASK_ENV", "development")
app.config.from_object(f"config.{env.title()}Config")

# Then instance-specific secrets from env vars
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]

# Use python-dotenv for local .env files
from dotenv import load_dotenv
load_dotenv()
20

Configuration

Config Object

Define config as classes inheriting a base Config. Override only what changes per environment. from_object copies uppercase attributes onto app.config, so lowercase helpers are ignored.

flask
class Config:
    DEBUG = False
    TESTING = False
    SECRET_KEY = "change-me"
    SQLALCHEMY_DATABASE_URI = "sqlite:///app.db"

class DevelopmentConfig(Config):
    DEBUG = True
    SECRET_KEY = "dev-secret"

class ProductionConfig(Config):
    DEBUG = False
    SECRET_KEY = os.environ["SECRET_KEY"]

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"

app.config.from_object(DevelopmentConfig)

Environment Variables

Use os.environ for secrets so they never live in code. from_envvar loads a Python file pointed to by an env var — useful for deployment-specific overrides. python-dotenv loads a .env file for local development.

flask
import os
from flask import Flask

app = Flask(__name__)

# Read individual values
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev")
app.config["DATABASE_URL"] = os.environ.get("DATABASE_URL", "sqlite:///app.db")

# Load a whole config file from an env var path
# export MYAPP_SETTINGS=/etc/myapp/prod.py
app.config.from_envvar("MYAPP_SETTINGS", silent=True)

# python-dotenv loads .env automatically if installed
from dotenv import load_dotenv
load_dotenv()

Instance Folder

The instance folder holds deployment-specific files (secrets, config) outside version control. Set instance_relative_config=True and load with from_pyfile. This separates code from environment configuration cleanly.

flask
# The instance/ folder sits outside the package, not in VCS.
# Flask searches it automatically for config files.

app = Flask(__name__, instance_relative_config=True)

# Load from instance/config.py
app.config.from_pyfile("config.py", silent=True)

# instance/config.py (deployment-specific, gitignored)
# SECRET_KEY = "real-production-key"
# SQLALCHEMY_DATABASE_URI = "postgresql://user:pass@db/app"

# Access the path
print(app.instance_path)  # /abs/path/to/instance

Config from File

from_object imports a module or object path; from_pyfile loads an absolute file; from_file (Flask 2.0+) uses a loader function, supporting JSON, TOML, or YAML. Pick one format and stay consistent.

flask
# config.py
DEBUG = True
SECRET_KEY = "dev"
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"

# Load a Python file
app.config.from_object("config")

# Load by path
app.config.from_pyfile("/etc/myapp/config.py")

# Load from a JSON file
app.config.from_file("config.json", load=json.load)

# config.json
# { "DEBUG": true, "SECRET_KEY": "dev" }

Development vs Production

Switch configs by environment variable so the same code runs everywhere. Production must disable debug (the debugger is a security hole) and use a real database. Log a warning if debug is on in production.

flask
import os
from flask import Flask

env = os.environ.get("FLASK_ENV", "development")

configs = {
    "development": DevelopmentConfig,
    "production": ProductionConfig,
    "testing": TestingConfig,
}

app = Flask(__name__)
app.config.from_object(configs[env])

# In production, never enable debug:
if app.config["DEBUG"]:
    print("WARNING: running in debug mode!")

Logging Configuration

Configure logging only when not in debug mode (Flask logs to stderr by default in debug). RotatingFileHandler prevents unbounded log growth. In production, consider structured JSON logging for aggregation services.

flask
import logging
from logging.handlers import RotatingFileHandler

if not app.debug:
    handler = RotatingFileHandler("app.log", maxBytes=10*1024*1024, backupCount=5)
    handler.setFormatter(logging.Formatter(
        "%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]"
    ))
    handler.setLevel(logging.INFO)
    app.logger.addHandler(handler)
    app.logger.setLevel(logging.INFO)

# In a view
@app.route("/")
def index():
    app.logger.info("Index page accessed")
    return "Hello"

# Flask's logger is a standard logging.Logger

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.