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?"
Source: Flask: Extensions
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 |
The guidelines ask for lower bounds such as |
Does it support the application factory and several apps in one process? |
The guidelines require |
Does it namespace its configuration keys and its entries in |
Everyone shares one |
Does it ship tests, a licence and documentation, and is it installable with |
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.
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
Source: Flask: Using 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
cacheinstance 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. Changingapp.configafter 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. |
|
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. |
|
Forms and CSRF |
Flask-WTF |
1.3.0 (2026-04-23) |
Pallets-Eco; active. |
|
Session login |
Flask-Login |
0.6.3 (2023-10-30) |
Widely used, classifier "Beta", no release for almost three years; stable API. |
|
Batteries-included security |
Flask-Security (Flask-Security-Too) |
5.9.1 (2026-09-30) |
Pallets-Eco; active; roles, two-factor, WebAuthn. |
|
Token authentication |
Flask-JWT-Extended |
4.7.4 (2026-05-13) |
Active. |
|
Server-side sessions |
Flask-Session |
0.8.0 (2024-03-26) |
Pallets-Eco; classifier "Beta". |
|
Caching |
Flask-Caching |
2.5.1 (2026-09-04) |
Pallets-Eco; active. |
the Caching page of this section |
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 |
the Internationalization with Flask-Babel page of this section |
CORS |
Flask-CORS |
6.0.5 (2026-06-08) |
Active. |
|
Security headers |
Flask-Talisman |
1.1.0 (2023-08-03) |
Stable, small, last released in 2023. |
|
Rate limiting |
Flask-Limiter |
4.1.1 (2025-12-06) |
Active. |
|
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". |
|
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. |
- 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
Source: Flask: Application Factories
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 |
Per-application state |
An object stored in |
Per-request state |
One attribute on |
Cleanup |
|
Configuration defaults |
|
Callbacks from users |
A decorator method ( |
Extras |
A blueprint with an inspection view and a Click command, both registered in |
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_appis the only place that sees the application object. Everything afterwards usescurrent_app,gandcurrent_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 throughapp.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.setdefaultgives defaults without overriding what the application set beforeinit_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
RuntimeErrorwith a helpful message replaces theKeyErrora forgotteninit_appwould 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.commandsentry point group is how a package contributes a command to theflaskexecutable 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 usesflit_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 |
|---|---|
|
The request object (default |
|
The response object (default |
|
How |
|
The configuration dict (default |
|
The type of |
|
The Werkzeug |
|
The Werkzeug |
|
The test client and CLI runner (both |
|
A dict of options passed to the Jinja environment (default empty). Must be changed before the first access of |
|
Overridable methods; |
Source: Flask: API, Application Object
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
Source: Flask: Subclassing Flask
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_apptwice on the same application replacesapp.extensions[name]and registers hooks and blueprints twice, which Flask reports as an error for duplicate blueprint names. Callinit_apponce in the factory. -
Reading
app.configat import time. Configuration values do not exist beforecreate_app()runs; reading them in module-level code fails or sees stale defaults. -
Using a view outside an application context.
current_app,gandcurrent_app.extensions[...]need an active application context; usewith app.app_context():in scripts and tests, andflask shellinteractively. -
Mutating
app.configafter the first request. Not guaranteed to be seen by other workers, as the guide says. -
Treating
gas 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 |
|---|---|
|
The |
|
Deprecated in 2.2 and gone in Flask 3.1 (the changelog told extension maintainers to use |
Extensions that wrote |
Not wrong for a single-app script but incompatible with the factory pattern and with the official guidelines; look for |
|
Recipe books of the Flask 1.x era install these. Flask has had its own |
Flask-RESTful and |
Still installable (last release 2023-05-21) but |
|
Older extension idioms replaced in current releases ( |
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): |
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 |
Flask 3.2 (unreleased): dispatch methods take the current |
Several |
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.