Extensions & Ecosystem

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Flask 3.1.3 (released 2026-02-18), Werkzeug 3.1.9 and Python 3.13, checked on 2026-10-07; every code block was executed with those versions. Flask 3.2 is not released yet; the one item of its changelog that matters here, the merge of the request context into the application context, is described as upcoming in What Changed and Where Older Material Is Outdated. Flask deliberately ships no database layer, no form library and no login system: the "micro" in its design (see Introduction & Architecture) is completed by extensions, ordinary Python packages that plug into the application object. This page explains how extensions work, how to find and judge them, which ones are maintained in 2026 for each common need, how to write one, and when subclassing Flask itself is the better tool. The running example is the Bookshelf application of the previous pages.

What an Extension Is

An extension is a package, usually named Flask-Something on PyPI and imported as flask_something, that adds behaviour to a Flask application through the same public setup methods an application uses itself: configuration values, request hooks, blueprints, CLI commands, template filters and context processors. There is no special registration protocol and no base class to inherit from. What makes a package a good Flask extension is a set of conventions, and the most important one is the init_app pattern described below. The official documentation describes extensions on the Extensions page and gives the authoring guidance on the Flask Extension Development page.

Many extensions are thin layers over a library that knows nothing about Flask: Flask-SQLAlchemy wraps SQLAlchemy, Flask-Migrate wraps Alembic, Flask-WTF wraps WTForms, Flask-Caching wraps cachelib, Flask-Limiter wraps limits. The extension’s job is the glue: reading app.config, creating the underlying object once per application, tying its lifetime to the request or application context, and exposing it through current_app, g or an import.

Finding Extensions

The official Extensions page gives one rule: extensions are usually named Flask-Foo or Foo-Flask, and PyPI can be searched for packages tagged with the trove classifier Framework :: Flask. That is the authoritative list, because the Flask maintainers retired the old "approved extensions" registry when it became too difficult to maintain; the guidelines used for approval are still published, and they remain a good checklist (see Vetting an Extension).

The second place to look is the Pallets-Eco organization on GitHub. It exists so that popular extensions can be maintained by the community, with oversight from the Pallets maintainers, when the original author moves on. Flask-SQLAlchemy, Flask-WTF, Flask-Session, Flask-Mail, Flask-Caching, Flask-Admin, Flask-Security, Flask-Alembic and Flask-DebugToolbar are in that situation. An extension living there is not a guarantee, but it means a handover path exists.

The classifier can also be read from the packages you already installed. The next example lists every installed distribution that declares Framework :: Flask, with the version and the minimum Python it declares; it is also a quick way to audit a project’s dependency set.

from importlib import metadata


def installed_flask_extensions():
    rows = []
    for dist in metadata.distributions():
        classifiers = dist.metadata.get_all("Classifier") or []
        if "Framework :: Flask" in classifiers:
            rows.append(
                (
                    dist.metadata["Name"],
                    dist.version,
                    dist.metadata.get("Requires-Python") or "unspecified",
                )
            )
    return sorted(rows, key=lambda row: row[0].lower())


for name, version, requires in installed_flask_extensions()[:5]:
    print(f"{name:28} {version:10} Python {requires}")

assert installed_flask_extensions(), "no Flask extension installed?"

Not every extension carries the classifier (some well-known ones omit it), so the search is a starting point, not a complete census.

Vetting an Extension

Because an extension runs inside your process with access to the request, the session and the configuration, it is a dependency to be chosen as carefully as Flask itself. The recommended guidelines of the official development page translate into questions you can answer in a few minutes before adding one:

Question Why it matters

Is there an active maintainer, and when was the last release?

An unmaintained extension stops following Flask, Werkzeug and SQLAlchemy releases. The tables below give the last release date as of 2026-10-07; a date years old is not automatically bad (a small, finished library may need no change) but it should make you read the issue tracker.

Does it declare a sane Requires-Python and lower bounds only?

The guidelines ask for lower bounds such as sqlalchemy>=1.4 and no upper bounds, so an extension does not block you from upgrading Flask. A package that pins <4 on Python (for example Flask-Babel’s <4.0) can still work, but it will conflict with a future major Python.

Does it support the application factory and several apps in one process?

The guidelines require init_app and the use of current_app rather than a stored application. An extension that needs a global app cannot be used with create_app() or with the test suite’s many applications.

Does it namespace its configuration keys and its entries in app.extensions and g?

Everyone shares one app.config, one app.extensions dict and one g. A prefix like CACHE_ or LIMITER_ prevents collisions.

Does it ship tests, a licence and documentation, and is it installable with pip install -e .?

These are the remaining guideline items, and they tell you whether you can fix a bug yourself.

Is it a security component?

Authentication, sessions, CSRF, CORS and header extensions deserve extra scrutiny; read the Security Considerations page and keep them updated.

The init_app Pattern

Every serious extension follows the same two-step protocol. First you create an extension instance with no application attached, at module level, in a file such as extensions.py. Later, inside create_app(), you call ext.init_app(app) to bind it to one concrete application. This is the shape the official Extensions page shows for an imaginary Flask-Foo, and it is what makes the application factory possible (the factory itself is explained in Application Factory & Project Layout).

The figure shows what happens in each phase and how a view later finds the extension’s per-application state.

flowchart LR A["extensions.py: cache = Cache(), db = SQLAlchemy()"] --> D B["app = Flask(__name__)"] --> C["app.config values"] C --> D["ext.init_app(app)"] D --> E["config defaults with setdefault"] D --> F["app.extensions name holds per-app state"] D --> G["hooks, blueprints, CLI commands"] H["view or helper at request time"] --> I["current_app.extensions name"] H --> J["g._name for per-request data"] F --> I
Figure 1. The init_app pattern: instance at import time, binding at factory time, lookup at request time

Using a real extension looks like this. The instance is created without an app, the factory configures the application and then initialises every extension, and the same instance can be imported by blueprints before any application exists. Note that app.extensions now holds one entry per initialised extension.

from flask import Flask
from flask_caching import Cache

# extensions.py: instances without an application
cache = Cache()


def create_app(test_config=None):
    app = Flask(__name__)
    app.config.from_mapping(CACHE_TYPE="SimpleCache", CACHE_DEFAULT_TIMEOUT=60)
    if test_config:
        app.config.from_mapping(test_config)

    cache.init_app(app)  # binds the extension to THIS application

    calls = {"count": 0}

    @app.get("/stats")
    @cache.cached(timeout=30)
    def stats():
        calls["count"] += 1
        return {"books": 42, "computed": calls["count"]}

    return app


app = create_app()
client = app.test_client()
first = client.get("/stats").get_json()
second = client.get("/stats").get_json()
print(first, second, sorted(app.extensions))
assert first == second  # the second response came from the cache
assert "cache" in app.extensions

Three consequences follow from the pattern, and they are the reason it is a requirement in the guidelines:

  • Several applications can coexist. The same cache instance is initialised for each application and keeps state per application, so a test suite can build a fresh app for every test.

  • No circular imports. Blueprints and models import extensions.py, never the module that creates the app.

  • Configuration is read once, at init_app. Changing app.config after the extension is initialised, or after the server starts handling requests, is not guaranteed to have an effect; the official guide adds that configuration is global and changes are not guaranteed to be visible to other workers.

Maintained Extensions by Need

The table lists, for each need that appears in a typical Flask project, the extension this section uses or points to. The Latest release column gives the version and date found on PyPI on 2026-10-07; Status is a judgement from those dates, the declared classifiers and the project pages, and is the first thing to re-check before you adopt anything. An older release date means "finished or slow", not "broken". The page that treats each extension in depth is linked in the last column.

Need Extension Latest release Status Where covered

SQL ORM

Flask-SQLAlchemy

3.1.1 (2023-09-11)

Pallets-Eco; stable. Flask-SQLAlchemy-Lite 0.2.1 (2025-11-20) is a lighter, newer alternative that exposes plain SQLAlchemy sessions.

Flask-SQLAlchemy

Migrations

Flask-Migrate

4.1.0 (2025-01-10)

Maintained by Miguel Grinberg; wraps Alembic. Flask-Alembic 3.3.0 (2026-10-05) is the Pallets-Eco alternative.

Database Migrations

Forms and CSRF

Flask-WTF

1.3.0 (2026-04-23)

Pallets-Eco; active.

Forms with Flask-WTF

Session login

Flask-Login

0.6.3 (2023-10-30)

Widely used, classifier "Beta", no release for almost three years; stable API.

Authentication & Flask-Login

Batteries-included security

Flask-Security (Flask-Security-Too)

5.9.1 (2026-09-30)

Pallets-Eco; active; roles, two-factor, WebAuthn.

Authorization & Roles

Token authentication

Flask-JWT-Extended

4.7.4 (2026-05-13)

Active.

JWT & API Authentication

Server-side sessions

Flask-Session

0.8.0 (2024-03-26)

Pallets-Eco; classifier "Beta".

Sessions, Cookies & Flashing

Caching

Flask-Caching

2.5.1 (2026-09-04)

Pallets-Eco; active.

the Caching page of this section

Mail

Flask-Mail

0.10.0 (2024-05-23)

Pallets-Eco; classifier "Beta". Flask-Mailman 1.1.1 (2024-07-06) is an alternative.

the Background Jobs, Celery & Email page of this section

Admin UI

Flask-Admin

2.2.1 (2026-09-21)

Pallets-Eco; active.

the Admin Interfaces with Flask-Admin page of this section

Internationalization

Flask-Babel

4.0.0 (2023-10-02)

Latest release is old; declares Requires-Python <4.0. Still the usual choice.

the Internationalization with Flask-Babel page of this section

CORS

Flask-CORS

6.0.5 (2026-06-08)

Active.

Single-Page Apps & CORS

Security headers

Flask-Talisman

1.1.0 (2023-08-03)

Stable, small, last released in 2023.

Security Considerations

Rate limiting

Flask-Limiter

4.1.1 (2025-12-06)

Active.

Security Considerations

WebSockets

flask-sock (plain WebSockets), Flask-SocketIO (Socket.IO)

0.7.0 (2023-10-02) and 5.6.1 (2026-02-21)

Both by Miguel Grinberg. flask-sock is small and stable; Flask-SocketIO is active.

the Streaming & Server-Sent Events page of this section

OpenAPI and validation

flask-smorest, APIFlask, flask-openapi3

0.47.0 (2026-03-22), 3.1.2 (2026-09-12), 4.3.2

All active. Flask-RESTX 1.3.2 (2025-09-23) is maintained but older in style; Flask-RESTful 0.3.10 (2023-05-21) is "code you will meet".

OpenAPI & API Documentation and Building REST APIs

Debugging

Flask-DebugToolbar

0.16.0 (2024-09-28)

Pallets-Eco; classifier "Beta"; development only.

the Debugging, Logging & Profiling page of this section

Role principals

Flask-Principal

0.4.0 (2013-06-14)

No release since 2013; prefer Flask-Security or hand-written decorators in new code.

Authorization & Roles

Source: https://pypi.org/search/?c=Framework+%3A%3A+Flask[PyPI: packages tagged Framework

Flask]

Two remarks about reading this table. First, several extensions are in this section’s examples in their "Beta" or older state because the alternative is hand-writing the same glue; choose by reading the project, not the classifier alone. Second, some entries are not extensions in the strict sense: Celery, RQ, Authlib, PyJWT, marshmallow, Pydantic and redis-py are plain libraries used from Flask (typically stored in app.extensions by your own factory code, as the NoSQL, Redis, MongoDB & Search page does with redis-py). A library does not need an extension wrapper to be used well.

Several Extensions in One Factory

A real project initialises its extensions in one place. This is the Bookshelf extensions.py plus factory fragment used by later pages: instances at module level, one init_app call each, and nothing that depends on a request. Extensions that need the application to exist before their own configuration is complete take the app as a constructor argument or a keyword (for example Babel(app, locale_selector=...) or the init_app method of LoginManager), so always read the extension’s page before assuming the signature.

import secrets

from flask import Flask
from flask_caching import Cache
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_login import LoginManager
from flask_talisman import Talisman

cache = Cache()
login_manager = LoginManager()
limiter = Limiter(key_func=get_remote_address)
talisman = Talisman()


def create_app(test_config=None):
    app = Flask(__name__)
    app.config.from_mapping(
        SECRET_KEY=secrets.token_hex(),  # a real app reads it from the environment
        CACHE_TYPE="SimpleCache",
        RATELIMIT_STORAGE_URI="memory://",
    )
    if test_config:
        app.config.from_mapping(test_config)

    cache.init_app(app)
    login_manager.init_app(app)
    limiter.init_app(app)
    talisman.init_app(app, force_https=False)  # force_https only disabled for the test client

    @app.get("/")
    def index():
        return {"service": "bookshelf"}

    return app


app = create_app()
response = app.test_client().get("/")
print(response.status_code, sorted(app.extensions))
assert response.status_code == 200
assert "Content-Security-Policy" in response.headers
assert {"cache", "limiter"} <= set(app.extensions)
assert app.login_manager is login_manager  # Flask-Login attaches itself differently

Registering in app.extensions is a convention, not a rule, and each extension chooses its own key (or none): the example shows that Flask-Caching and Flask-Limiter register themselves there, Flask-Login attaches the manager as app.login_manager, and Flask-Talisman only installs request hooks. That is why the example prints the keys instead of assuming them. Flask-Limiter documents its configuration in app.config keys prefixed RATELIMIT_, Flask-Caching uses CACHE_, Flask-Login mostly uses attributes of the LoginManager; there is no common prefix rule except "start with the extension’s name", which the official guide recommends.

Writing an Extension

Writing your own extension is worth it when several of your projects, or several teams, need the same integration. For code used by one application, a plain module or a blueprint is simpler and the pattern below is optional. The rules come from the official Flask Extension Development page; the example builds a small AuditLog extension for Bookshelf that records one entry per request, lets other code attach notes to the current request, and lets applications register listeners.

The design decisions it illustrates are listed first so the code can be read against them:

Concern Decision

Entry point

A class with __init__(self, app=None) and init_app(self, app). The application is never stored on the extension (self.app = app is explicitly discouraged by the guide), because that breaks several applications in one process and the factory.

Per-application state

An object stored in app.extensions["audit"] under a name that is unique to the extension (the guide suggests the extension’s name without the flask prefix). Helpers reach it with current_app.extensions["audit"].

Per-request state

One attribute on g with an underscore prefix and the extension’s name, g._audit. g is a single namespace shared with the application, so a prefix avoids collisions. The data lasts for the application context.

Cleanup

app.teardown_appcontext(...) for something that must be released when the application context ends (requests, CLI commands and scripts alike); teardown_request if it only makes sense inside a request.

Configuration defaults

app.config.setdefault("AUDIT_...", value) in init_app, with keys prefixed by the extension name, so applications override them before or after init_app without being forced to define them.

Callbacks from users

A decorator method (@audit.listener) on the extension instance. Calls go through current_app.ensure_sync(function), so listeners can be plain functions or async def functions.

Extras

A blueprint with an inspection view and a Click command, both registered in init_app with the normal Flask methods.

import time
from types import SimpleNamespace

import click
from flask import Blueprint, Flask, current_app, g, jsonify, request


class _AuditState:
    """Everything the extension keeps per application."""

    def __init__(self, max_entries):
        self.entries = []
        self.max_entries = max_entries


class AuditLog:
    def __init__(self, app=None):
        self._listeners = []  # per extension instance, not per application
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        # defaults: key names start with the extension name
        app.config.setdefault("AUDIT_ENABLED", True)
        app.config.setdefault("AUDIT_MAX_ENTRIES", 1000)
        app.config.setdefault("AUDIT_SKIP_PREFIXES", ("/static", "/_audit"))

        # per-application state under a unique key
        app.extensions["audit"] = _AuditState(app.config["AUDIT_MAX_ENTRIES"])

        app.before_request(self._begin)
        app.after_request(self._finish)
        app.teardown_appcontext(self._cleanup)

        blueprint = Blueprint("audit", __name__, url_prefix="/_audit")
        blueprint.add_url_rule("/", view_func=self._entries_view)
        app.register_blueprint(blueprint)

        @app.cli.command("audit-count")
        def audit_count():
            """Print how many entries this process holds."""
            click.echo(len(self.state().entries))

    # -- helpers used by views, always through current_app --
    @staticmethod
    def state():
        try:
            return current_app.extensions["audit"]
        except KeyError:
            raise RuntimeError(
                "AuditLog is not initialised: call audit.init_app(app) in create_app()"
            ) from None

    def listener(self, function):
        """Register a function (or coroutine function) called with each entry."""
        self._listeners.append(function)
        return function

    def note(self, text):
        if "_audit" in g:
            g._audit.notes.append(text)

    # -- hooks --
    def _begin(self):
        if current_app.config["AUDIT_ENABLED"]:
            g._audit = SimpleNamespace(started=time.perf_counter(), notes=[])

    def _finish(self, response):
        context = g.get("_audit")
        skip = current_app.config["AUDIT_SKIP_PREFIXES"]
        if context is None or request.path.startswith(skip):
            return response
        entry = {
            "method": request.method,
            "path": request.path,
            "status": response.status_code,
            "notes": list(context.notes),
            "ms": round((time.perf_counter() - context.started) * 1000, 3),
        }
        state = self.state()
        state.entries.append(entry)
        del state.entries[: -state.max_entries]
        for function in self._listeners:
            current_app.ensure_sync(function)(entry)  # sync or async
        return response

    def _cleanup(self, exception):
        g.pop("_audit", None)  # nothing to close here; shows where cleanup goes

    def _entries_view(self):
        return jsonify(self.state().entries)


audit = AuditLog()
seen = []


@audit.listener
def remember(entry):
    seen.append(entry["path"])


@audit.listener
async def remember_async(entry):  # needs flask[async], that is asgiref
    seen.append("async:" + entry["path"])


def create_app(**overrides):
    app = Flask(__name__)
    app.config.update(overrides)
    audit.init_app(app)

    @app.get("/books/<int:book_id>")
    def book(book_id):
        audit.note(f"book {book_id} requested")
        return {"id": book_id}

    return app


app = create_app()
client = app.test_client()
client.get("/books/7")
entries = client.get("/_audit/").get_json()
print(entries)
assert entries[0]["path"] == "/books/7" and entries[0]["notes"] == ["book 7 requested"]
assert seen == ["/books/7", "async:/books/7"]
assert not any(e["path"].startswith("/_audit") for e in entries)

Several details of this code are deliberate and each follows a statement in the official guide:

  • init_app is the only place that sees the application object. Everything afterwards uses current_app, g and current_app.extensions.

  • The state in app.extensions["audit"] belongs to one application; the listeners belong to the extension instance. That split is the "configuration per extension instance through decorator methods" technique of the guide, next to "configuration per application through app.config`". Choose per option which one it is: anything that could change from one deployment to another belongs in `app.config; things that define how the extension is used belong on the instance.

  • app.config.setdefault gives defaults without overriding what the application set before init_app.

  • Cleanup goes in teardown_appcontext, which also runs for CLI commands and scripts, not only for web requests; the Hooks & View Decorators and Application & Request Lifecycle pages explain when each teardown fires.

  • The RuntimeError with a helpful message replaces the KeyError a forgotten init_app would produce.

Calling User Callbacks with ensure_sync

Since Flask 2.0, views, hooks and handlers can be async def functions when the async extra of Flask is installed (it pulls in asgiref). When an extension invokes a callback the user registered, it cannot know which kind it got. current_app.ensure_sync(function) returns the function itself when it is a plain def and a synchronous wrapper when it is a coroutine function, so the extension works with both. Flask-Login and Flask-Security call user callbacks this way. The mechanism, the cost of the event loop created per call, and gevent and Quart alternatives are covered by the Async, gevent & Quart page of this section; for extension authors the rule is simply: never call a user-supplied callable directly, always wrap it with ensure_sync, and never block inside an async callback that you call yourself.

Testing the Extension Against Several Applications

The test that proves the pattern works is to initialise the same extension instance for two applications with different configuration and check that nothing leaks between them. A second test covers the helpful error. The Testing page of this section covers pytest fixtures in general; these two checks are the extension-specific part.

def test_two_applications_are_independent():
    first = create_app(AUDIT_MAX_ENTRIES=1)
    second = create_app(AUDIT_MAX_ENTRIES=5)

    for _ in range(3):
        first.test_client().get("/books/1")
    second.test_client().get("/books/2")

    first_entries = first.test_client().get("/_audit/").get_json()
    second_entries = second.test_client().get("/_audit/").get_json()
    assert len(first_entries) == 1  # AUDIT_MAX_ENTRIES=1 trimmed the log
    assert [e["path"] for e in second_entries] == ["/books/2"]


def test_missing_init_app_has_a_clear_error():
    import pytest

    bare = Flask(__name__)
    with bare.app_context():
        with pytest.raises(RuntimeError, match="init_app"):
            AuditLog.state()


def test_cli_command_is_registered():
    app = create_app()
    app.test_client().get("/books/3")
    result = app.test_cli_runner().invoke(args=["audit-count"])
    assert result.exit_code == 0 and result.output.strip() == "1"


test_two_applications_are_independent()
test_missing_init_app_has_a_clear_error()
test_cli_command_is_registered()
print("extension tests passed")

Request Data, Views and Models

The guide’s Data During a Request section is the reason for the g._audit name. Two alternatives it shows are the prefixed attribute (g._hello_user_id) and a namespace object (g._hello = SimpleNamespace()), used above. Before Flask 2.2 many extensions stored data on _app_ctx_stack.top; those private stacks were deprecated in 2.2, their push and pop were removed in 2.3, and they are not available in Flask 3.1; the changelog told extension maintainers to store data on g with a unique prefix instead. If you read an old extension that touches _app_ctx_stack, it is out of date and probably broken on Flask 3.

The guide’s Views and Models section covers the harder case where an extension’s views need a model that the application defines (for example a Flask-SimpleBlog extension on top of Flask-SQLAlchemy): pass the model into a class-based view with as_view(), or keep it on the extension and read it back from current_app.extensions[name], or provide a base class the application subclasses. The guide itself notes there is no perfect solution; the class-based views mentioned are in Class-Based Views. Its sample uses the legacy Model.query API, which the data pages of this section replace with db.session.get() and db.session.execute(db.select(...)).

Packaging and Naming

The guide asks for the name Flask-ExtensionName or ExtensionName-Flask on the index, exactly one package or module named flask_extension_name, an open source licence (BSD or MIT are the norm in this ecosystem), tests runnable with pytest, a documentation link in the metadata, lower bounds only on dependencies and a requires-python lower bound. With a modern pyproject.toml that is:

[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"

[project]
name = "Flask-AuditLog"
version = "0.1.0"
description = "Per-request audit entries for Flask applications."
readme = "README.md"
license = { text = "BSD-3-Clause" }
requires-python = ">=3.10"
dependencies = ["flask>=3.0"]
classifiers = [
    "Framework :: Flask",
    "Programming Language :: Python :: 3",
]

[project.urls]
Documentation = "https://example.org/flask-auditlog"

# optional: make `flask audit` discoverable without the app importing the package
[project.entry-points."flask.commands"]
audit = "flask_auditlog.cli:audit_group"

[project.optional-dependencies]
async = ["flask[async]"]
tests = ["pytest"]
The `Framework

Flask` classifier makes the package discoverable through the PyPI search of the previous section. The flask.commands entry point group is how a package contributes a command to the flask executable without the application registering it; it is covered on the CLI & Shell page of this section. The project metadata above is only configuration, so the block is not executed; it follows the packaging guidance of the Application Factory & Project Layout page (the Flask tutorial uses flit_core, other build backends work equally well).

Subclassing Flask

Before writing an extension for a change, check whether it is really a change to the Flask object itself. Flask exposes a family of class attributes that name the classes it instantiates. Replacing one on a subclass is supported, documented and much less fragile than monkey-patching. The attributes found in Flask 3.1.3 are:

Attribute What it replaces

request_class

The request object (default flask.wrappers.Request, a Werkzeug request); add properties or change limits.

response_class

The response object (default flask.wrappers.Response); change default headers or the default mimetype.

json_provider_class

How jsonify and app.json serialize (default DefaultJSONProvider); flask-orjson is an extension built on this.

config_class

The configuration dict (default flask.config.Config).

app_ctx_globals_class

The type of g, for example to give it defaults.

url_map_class, url_rule_class

The Werkzeug Map and Rule used for routing.

aborter_class

The Werkzeug Aborter behind abort().

test_client_class, test_cli_runner_class

The test client and CLI runner (both None by default, which uses Flask’s own); add helper methods for tests.

jinja_options

A dict of options passed to the Jinja environment (default empty). Must be changed before the first access of app.jinja_env.

ensure_sync, make_config, select_jinja_autoescape and other methods

Overridable methods; ensure_sync is how an application could run async views differently.

The next example subclasses Flask to give the whole Bookshelf application a request id, a standard response header and a test client with a small helper. Compare it with the alternative: the same effect can be built with an after_request hook, which is simpler and per-application; subclassing is justified when the change is a property of the class of objects (for example a custom request type used in many places), when you ship a base application for several projects, or when you must change behaviour no hook can reach, such as the JSON provider or the routing map.

import secrets

from flask import Flask, Request, Response, request
from flask.testing import FlaskClient
from werkzeug.utils import cached_property


class BookshelfRequest(Request):
    @cached_property
    def request_id(self):
        return self.headers.get("X-Request-ID") or secrets.token_hex(8)


class BookshelfResponse(Response):
    default_mimetype = "application/json"


class BookshelfClient(FlaskClient):
    def get_json_ok(self, url, **kwargs):
        response = self.get(url, **kwargs)
        assert response.status_code == 200, response.status_code
        return response.get_json()


class BookshelfFlask(Flask):
    request_class = BookshelfRequest
    response_class = BookshelfResponse
    test_client_class = BookshelfClient
    jinja_options = {"trim_blocks": True, "lstrip_blocks": True}

    def process_response(self, response):
        response = super().process_response(response)
        response.headers["X-Request-ID"] = request.request_id
        return response


def create_app():
    app = BookshelfFlask(__name__)

    @app.get("/ping")
    def ping():
        return {"request_id": request.request_id}

    return app


app = create_app()
client = app.test_client()
body = client.get_json_ok("/ping", headers={"X-Request-ID": "abc123"})
print(body, isinstance(client, BookshelfClient))
assert body == {"request_id": "abc123"}
assert client.get("/ping").headers["X-Request-ID"]
assert app.jinja_env.trim_blocks is True

Two cautions apply. First, a subclass is a public commitment: other extensions receive your application and expect it to behave like Flask, so call super() in every override. Second, process_response is documented as the method that by default calls all after_request functions; override it only when a hook cannot do the job, and always call the parent method first as above. The sibling pages on Responses & JSON and Request Data show the usual ways to customise a response or read a request without a subclass.

Pitfalls

  • Importing the extension module and calling init_app twice on the same application replaces app.extensions[name] and registers hooks and blueprints twice, which Flask reports as an error for duplicate blueprint names. Call init_app once in the factory.

  • Reading app.config at import time. Configuration values do not exist before create_app() runs; reading them in module-level code fails or sees stale defaults.

  • Using a view outside an application context. current_app, g and current_app.extensions[...] need an active application context; use with app.app_context(): in scripts and tests, and flask shell interactively.

  • Mutating app.config after the first request. Not guaranteed to be seen by other workers, as the guide says.

  • Treating g as a cache across requests. It lives for one application context, normally one request. Use a real cache (the Caching page of this section) for cross-request data.

  • Storing the application on the extension. It breaks the factory pattern and the test suite.

  • Choosing an extension for a one-line feature. If a template filter or a before_request hook does the job, do not add a dependency.

  • Trusting an extension with security duties without reading it. Verify the defaults (cookie flags, CSRF, CORS origins, header policies) against the Security Considerations page.

What Changed and Where Older Material Is Outdated

Item Current state

flask.ext.sqlalchemy imports

The flask.ext namespace was deprecated in Flask 0.11 and removed in Flask 1.0. Import the real package, import flask_sqlalchemy. Old tutorials using from flask.ext... do not run.

_app_ctx_stack, _request_ctx_stack in extensions

Deprecated in 2.2 and gone in Flask 3.1 (the changelog told extension maintainers to use g with a prefix). The contexts are backed by contextvars.

Extensions that wrote self.app = app

Not wrong for a single-app script but incompatible with the factory pattern and with the official guidelines; look for init_app.

Flask-Script, Flask-WhooshAlchemy, git+git:// installs

Recipe books of the Flask 1.x era install these. Flask has had its own flask CLI since 0.11 (the CLI & Shell page of this section), the git:// protocol is no longer served by GitHub, and Whoosh-based search is replaced in this section by an Elasticsearch or database approach (see NoSQL, Redis, MongoDB & Search).

Flask-RESTful and reqparse

Still installable (last release 2023-05-21) but reqparse is deprecated by its own documentation; use flask-smorest, APIFlask or plain MethodView (see Building REST APIs).

@babel.localeselector, CACHE_TYPE = "simple", make_celery

Older extension idioms replaced in current releases (Babel(app, locale_selector=...), "SimpleCache", the celery_init_app helper); each is covered on the page of its extension.

Eventlet instead of gevent

The Flask documentation states that eventlet is no longer maintained; the Async, gevent & Quart page of this section covers gevent.

Flask 3.2 (unreleased): RequestContext merged into AppContext

In the unreleased 3.2 changelog the request context becomes a deprecated alias of the application context, and Flask no longer reuses an already pushed application context when dispatching. Extensions that rely only on current_app, g and request are unaffected; extensions that touch RequestContext or push contexts manually should be tested on the 3.2 release when it appears.

Flask 3.2 (unreleased): dispatch methods take the current AppContext

Several Flask dispatch methods (the ones a subclass may override) take the current application context as first argument in the unreleased version; Flask detects the old signatures and emits a deprecation warning. Subclasses that override process_response, full_dispatch_request and similar methods should be re-checked on 3.2.

Source: Flask: Changes

Where to Go Next

The Bookshelf extensions each have a dedicated page: ORM and migrations (Flask-SQLAlchemy, Database Migrations), forms (Forms with Flask-WTF), authentication (Authentication & Flask-Login) and the pages of the later groups on the command line, signals, administration, translations, caching and background jobs. For protocols and infrastructure behind extensions (message brokers, schedulers, Redis) see Messaging, Scheduling and Redis.