pallets/flask
github.com/pallets/flask ↗ · 230 files · 1.1 MB
Flask Flask is a lightweight [WSGI] web application framework.
Overview
Flask is the Pallets WSGI web framework — a thin, opinion-light composition layer over Werkzeug (routing, request/response, dev server), Jinja (templating), itsdangerous (signed session cookies), Click (CLI) and Blinker (signals), distributed as the `flask` package plus a `flask` console script. This checkout is the unreleased 3.2.0.dev line on `main`, whose headline change is the merger of `RequestContext` into a single `AppContext`: one context object now carries app, `g`, request, session and URL adapter, is pushed once per request and per CLI command, and is threaded explicitly as a `ctx` first argument through `dispatch_request`, `full_dispatch_request`, `finalize_request`, `preprocess_request`, `process_response` and the teardown methods. The codebase is deliberately split into a sans-IO core (`src/flask/sansio/`) that alternative implementations such as Quart reuse, and IO-bound subclasses (`app.py`, `blueprints.py`) that add WSGI, Click groups and file serving. It targets Python 3.10+, is fully typed (`py.typed`, strict mypy + pyright), and ships its own tutorial, JavaScript and Celery example applications.
System map
The library entrypoints are the `flask` package namespace (`src/flask/__init__.py` re-exports the public API), the `flask` console script wired to `flask.cli:main`, and `python -m flask` via `__main__.py`. Runtime is split across a sans-IO layer (`Scaffold` → `App` → `Blueprint` in `src/flask/sansio/`) that holds registries and setup logic with no IO, and the IO layer (`Flask`, `Blueprint`, `cli`, `helpers`, `templating`) that performs WSGI dispatch, file serving and server startup. There is no database or persistence layer; the only durable state Flask owns is the signed session cookie written by `SecureCookieSessionInterface`. External integrations are all Pallets-family libraries pinned in `pyproject.toml` dependencies, plus optional `asgiref` (async views) and `python-dotenv` (env files). Build is flit_core; test/lint/type/docs orchestration is tox driven by uv, executed in GitHub Actions.
- `src/flask/__init__.py` is the public API surface: 39 explicit `X as X` re-exports covering `Flask`, `Blueprint`, `Config`, the five context proxies, all 10 signals, and the `render_template`/`stream_template` family — nothing is exported implicitly.
- The console script is declared as `flask = "flask.cli:main"` under `[project.scripts]`, and `src/flask/__main__.py` is a three-line shim (`from .cli import main; main()`) so `python -m flask` and `flask` share one code path.
- The sans-IO boundary is documented in-tree: `src/flask/sansio/README.md` states the folder must do no IO, must not be on a likely IO path, and must not use the Flask globals — which is why `sansio/app.py` declares `default_config` and `response_class` as bare annotations and lets `Flask` in `app.py` supply them.
- Class hierarchy is `Scaffold` (shared route/handler registration, static folder, `find_package`) → `App` (config, url_map, blueprints dict, extensions dict, template filters/tests/globals) → `Flask` (WSGI, Click group, sessions, file serving); `Blueprint` follows the same sansio → IO split.
- All context state lives in one `AppContext` object holding `app`, `g`, `url_adapter`, `_request`, `_session`, `_flashes` and `_after_request_functions`, stored in the single `ContextVar` `_cv_app` declared in `globals.py`; there is no second request-level ContextVar.
- The five public proxies (`app_ctx`, `current_app`, `g`, `request`, `session`) are all `werkzeug.local.LocalProxy` instances over the same `_cv_app`, differing only by attribute name and unbound-error message (`_no_app_msg` vs `_no_req_msg`).
- The only persistence layer is the signed cookie: `SecureCookieSessionInterface` builds a `URLSafeTimedSerializer` with salt `"cookie-session"`, hmac key derivation, lazily-resolved SHA-1 digest, and the `TaggedJSONSerializer` payload serializer.
- Runtime dependencies are exactly six Pallets-ecosystem packages (blinker≥1.9.0, click≥8.1.3, itsdangerous≥2.2.0, jinja2≥3.1.2, markupsafe≥2.1.1, werkzeug≥3.1.0) with `async` (asgiref) and `dotenv` (python-dotenv) as optional extras.
- Build backend is `flit_core>=3.11,<4` with `[tool.flit.module] name = "flask"`, and the sdist explicitly bundles `docs/`, `examples/`, `tests/`, `CHANGES.rst` and `uv.lock` while excluding `docs/_build/`.
- Developer commands are tox environments: `tox run` (pytest per Python version), `-e style` (pre-commit all files), `-e typing` (mypy then pyright), `-e docs` (`sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml`), plus `tests-min` and `tests-dev` which pip-install pinned-minimum and git-main dependency sets respectively.
- CI is four GitHub Actions workflows: `tests.yaml` (12-way matrix incl. 3.15, free-threaded 3.14t, Windows, Mac, PyPy, min and dev deps, plus a separate mypy/pyright `typing` job), `pre-commit.yaml`, `publish.yaml` (tag-triggered `uv build` → draft GH release → PyPI trusted publishing), and `zizmor.yaml` for Actions security analysis.
- Docs are Sphinx `dirhtml` built by Read the Docs using an asdf-installed uv and the `docs` dependency group; 34 top-level doc pages exist including `lifecycle.rst`, `appcontext.rst`, `reqcontext.rst` and `web-security.rst`.
Feature catalog
As a framework, Flask's user-facing features are its decorator registries, its CLI subcommands, and its request/response conveniences. Route registration spans `@route` plus seven method shortcuts including the newly added `@app.query` for HTTP QUERY; class-based views come via `View`/`MethodView`. The `flask` CLI ships `run`, `shell` and `routes` built in, discovers app-provided commands through `app.cli`/`blueprint.cli`, and loads third-party commands from the `flask.commands` entry point group. Response handling covers strings, bytes, dicts, lists, generators, tuples, WSGI callables and Response objects; JSON is routed through a swappable `JSONProvider`. Sessions, flashing, file sending, streaming, signals and blueprint nesting round out the surface.
- Seven HTTP-method route shortcuts are generated through `Scaffold._method_route`: `get`, `query`, `post`, `put`, `delete`, `patch` (plus `route`); `query` is new in 3.2 for the HTTP QUERY method and is also listed in `views.http_method_funcs`.
- `flask run` exposes `--host/-h` (default 127.0.0.1), `--port/-p` (5000), `--cert`/`--key`, `--reload/--no-reload`, `--debugger/--no-debugger`, `--with-threads/--without-threads`, `--extra-files` and `--exclude-patterns`, and inserts `--debug` as its first parameter after definition.
- `flask shell` builds an interactive REPL banner from `current_app.import_name` and `instance_path`, honours `PYTHONSTARTUP`, wires `rlcompleter.Completer` against the shell context rather than `__main__`, then calls `code.interact`.
- `flask routes` supports `--sort` over `endpoint|methods|domain|rule|match` and `--all-methods`, hides HEAD/OPTIONS by default, and renders a padded ASCII table computed from per-column max widths.
- The root CLI group is `FlaskGroup`, which registers three eager options in order (`--env-file`, `--app`, `--debug`) so dotenv loading happens before app import, and sets `auto_envvar_prefix="FLASK"` so every option is also settable via `FLASK_*` env vars.
- App discovery supports four forms: explicit `module:name`, `module:factory(args)` with `ast.literal_eval`-parsed arguments, auto-detection of `app`/`application`/`create_app`/`make_app`, and implicit `wsgi.py` then `app.py` in the CWD.
- `Flask.make_response` accepts str/bytes/bytearray, any iterator (streaming), dict and list (auto-JSON via `self.json.response`), `BaseResponse` subclasses, arbitrary WSGI callables, and 2- or 3-tuples of `(body, status)`, `(body, headers)` or `(body, status, headers)`.
- Class-based views: `View.as_view` supports `init_every_request=False` for a shared instance, applies `cls.decorators` in order, and copies `methods`/`provide_automatic_options` onto the generated function; `MethodView.__init_subclass__` auto-derives `methods` from defined handler names and falls back from HEAD to GET.
- Message flashing writes `session["_flashes"]` via a read-append-reassign pattern (deliberately not `setdefault`, so external session backends stay in sync) and emits the `message_flashed` signal; `get_flashed_messages` caches into `app_ctx._flashes` so repeat calls in one request are stable.
- Ten Blinker signals are defined in one namespace: template-rendered, before-render-template, request-started, request-finished, request-tearing-down, got-request-exception, appcontext-tearing-down, appcontext-pushed, appcontext-popped and message-flashed.
- Async views are opt-in: `ensure_sync` returns plain functions unchanged and wraps coroutine functions via `async_to_sync`, which imports `asgiref.sync.async_to_sync` and raises "Install Flask with the 'async' extra" if asgiref is missing.
- Config can be populated six ways: `from_envvar`, `from_prefixed_env` (JSON-decoded `FLASK_*` vars with `__` nesting), `from_pyfile` (exec'd as a module), `from_object` (uppercase attrs only), `from_file` (pluggable loader, text or binary) and `from_mapping`; `get_namespace` extracts prefixed subsets.
Data flow
Input boundaries are the WSGI environ, the process environment (`FLASK_*`, dotenv files), config files, and the request cookie jar. A WSGI environ becomes a `Request` in `AppContext.from_environ`, which also stamps `request.json_module = app.json` so body JSON decoding uses the app's provider. The `AppContext` is the single state owner for the duration of a request: it holds `g`, the lazily opened session, the bound `MapAdapter`, collected `after_this_request` callbacks and cached flashes. Output boundaries are the `Response` returned to the WSGI server, the `Set-Cookie` session header written in `process_response`, rendered template strings, and log records emitted to `wsgi.errors` or stderr. Config flows one way: `default_config` → `Config` dict → `ConfigAttribute` descriptors (`app.testing`, `app.secret_key`, `app.permanent_session_lifetime`) and per-request property lookups on `Request`/`Response`.
- The environ→Request transform happens in `AppContext.from_environ`, which constructs `app.request_class(environ)` and immediately assigns `request.json_module = app.json`, so `request.get_json()` uses the app's JSON provider rather than stdlib json.
- The URL adapter is built in `AppContext.__init__` (not at push time) via `app.create_url_adapter`, and any `HTTPException` raised there is stashed on `request.routing_exception` instead of propagating.
- `create_url_adapter` applies `TRUSTED_HOSTS` to `request.trusted_hosts` and then recomputes `request.host` through `werkzeug.wsgi.get_host` before binding — host validation happens on this path, before routing.
- Session data flows cookie → `SecureCookieSessionInterface.open_session` → `URLSafeTimedSerializer.loads(val, max_age=PERMANENT_SESSION_LIFETIME)` → `SecureCookieSession`; a `BadSignature` silently yields a fresh empty session rather than an error.
- Session writes are deferred to `process_response`, which calls `save_session` only when `is_null_session` is false, and `save_session` adds `Vary: Cookie` whenever `session.accessed` is set, deletes the cookie when an emptied session is modified, and otherwise gates on `should_set_cookie`.
- Session values are serialized by `TaggedJSONSerializer`, which round-trips non-JSON Python types through single-key tag dicts — ` di` (dict), ` t` (tuple), ` b` (base64 bytes), ` m` (Markup), ` u` (UUID), ` d` (http-date datetime).
- Config resolution order is `default_config` copy → `DEBUG` overwritten from `get_debug_flag()` (the `FLASK_DEBUG` env var) → `Config(root_path, defaults)`, with `root_path` swapped to `instance_path` when `instance_relative_config=True`.
- Env-file input precedence is: variables already in `os.environ` win, then `-e/--env-file`, then `.env`, then `.flaskenv` — implemented by merging dicts in that order in `load_dotenv` and skipping keys already present in `os.environ`.
- Per-request body limits read through `current_app.config` at access time rather than being copied onto the request: `max_content_length`, `max_form_memory_size` and `max_form_parts` each check a private override, then fall back to `MAX_CONTENT_LENGTH` / `MAX_FORM_MEMORY_SIZE` (500_000) / `MAX_FORM_PARTS` (1_000).
- Template context is assembled in `update_template_context`: app-level then reversed blueprint-level context processors run, and the caller's explicit `render_template(**context)` values are re-applied last from a saved copy so they always win.
- The default template context processor replaces the `request` and `g` proxies with concrete objects for speed but deliberately leaves `session` as a proxy, because touching it would set `session.accessed` and force a `Vary: Cookie` header.
- `send_file`/`send_from_directory` funnel through `_prepare_send_file_kwargs`, which injects `environ`, `USE_X_SENDFILE`, `response_class`, `_root_path` and defaults `max_age` to `app.get_send_file_max_age` before delegating to Werkzeug.
Behavior traces
Three flows dominate: application construction, WSGI request dispatch, and CLI invocation. Construction is `Flask.__init__` → `App.__init__` → `Scaffold.__init__`, ending with an auto-registered static route that holds only a weakref to the app. Request dispatch is `__call__` → `wsgi_app` → `AppContext.push` (session open, then routing) → `full_dispatch_request` → `preprocess_request` → `dispatch_request` → `finalize_request` → `process_response` (session save) → `ctx.pop` (teardowns). CLI invocation is `main` → `FlaskGroup.make_context` (sets `FLASK_RUN_FROM_CLI`) → eager `--env-file`/`--app` callbacks → `ScriptInfo.load_app` → command body with an app context pushed via `ctx.with_resource`.
- Startup: `Flask.__init__` calls `super().__init__` (App → Scaffold), creates `self.cli = cli.AppGroup()` named after the app, then — if `has_static_folder` — asserts `bool(static_host) == host_matching` and registers `f"{static_url_path}/<path:filename>"` with a `weakref.ref(self)` closure to avoid an app↔view reference cycle (issue #3761).
- Startup: `App.__init__` resolves `instance_path` (rejecting relative paths), builds `config` via `make_config`, instantiates `aborter`, `json_provider_class(self)`, the empty `blueprints`/`extensions` dicts, `url_map_class(host_matching=...)`, and sets `_got_first_request = False`.
- Request: `Flask.__call__` → `wsgi_app(environ, start_response)` creates the context with `self.request_context(environ)`, pushes it, dispatches, and in `finally` restores it for the debugger via `environ["werkzeug.debug.preserve_context"]` before `ctx.pop(error)`.
- Request: `AppContext.push` increments `_push_count`, returns early if already pushed, sets `_cv_app`, sends `appcontext_pushed`, then opens the session via `_get_session()` *before* calling `match_request()` — ordering that lets custom URL converters read the session.
- Request: `full_dispatch_request` sets `_got_first_request = True` (freezing setup methods), sends `request_started`, runs `preprocess_request`, short-circuits to `finalize_request` if a `before_request` returned non-None, otherwise calls `dispatch_request`, catching exceptions into `handle_user_exception`.
- Request: `preprocess_request` runs `url_value_preprocessors` for `(None, *reversed(req.blueprints))` first, then `before_request_funcs` in the same order — app-level before blueprint-level, outermost blueprint first.
- Request: `dispatch_request` raises any stored `routing_exception` through `raise_routing_exception`, returns `make_default_options_response` for automatic OPTIONS, and otherwise calls `ensure_sync(self.view_functions[rule.endpoint])(**view_args)`.
- Request: `process_response` runs `ctx._after_request_functions` (from `after_this_request`) first, then app+blueprint `after_request_funcs` in reverse registration order over `chain(request.blueprints, (None,))`, then saves the session.
- Teardown: `AppContext.pop` decrements `_push_count`, returns while still nested, then under `_CollectErrors` runs `do_teardown_request`, `request.close()`, `do_teardown_appcontext`, resets the ContextVar, sends `appcontext_popped`, and finally re-raises collected errors as a `BaseExceptionGroup` on Python 3.11+.
- Error path: `handle_exception` sends `got_request_exception`, re-raises when `PROPAGATE_EXCEPTIONS` (defaulting to `testing or debug`) is truthy, otherwise logs, wraps in `InternalServerError(original_exception=e)`, looks up a 500 handler, and finalizes with `from_error_handler=True` so response-processing failures are logged rather than raised.
- Error handler lookup in `_find_error_handler` tries (specific code, then None) × (each blueprint name, then None), walking `exc_class.__mro__` at each step — so a blueprint handler for a specific code beats an app handler for that code, which beats class-based handlers.
- CLI: `FlaskGroup.make_context` sets `os.environ["FLASK_RUN_FROM_CLI"] = "true"` so a stray module-level `app.run()` becomes a no-op that prints a red warning instead of blocking the command.
Testing signals
Testing is pytest-only, rooted at `tests/` with `testpaths = ["tests"]` and `filterwarnings = ["error"]` so any unexpected warning fails the suite — a notable constraint given 3.2's new deprecation warnings. 22 test modules totalling ~7.6k lines cover the framework, with `test_basic.py` (1974 lines, 95 tests), `test_blueprints.py` (1115) and `test_cli.py` (703) dominating. Fixtures in `conftest.py` provide `app`, `client`, `app_ctx`, `req_ctx`, plus two autouse guards: `_standard_os_environ` clears FLASK_* env vars, and `leak_detector` asserts no app context is left pushed at test end. Static typing is treated as a test surface of its own: `tests/type_check/` holds three assertion-free modules checked by strict mypy and pyright in a dedicated CI job. Coverage is configured with branch mode over `flask` and `tests`, but no coverage gate is enforced in any workflow.
- pytest config is minimal and strict: `testpaths = ["tests"]` with `filterwarnings = ["error"]`, so every warning — including the new `RequestContext`/`should_ignore_error` DeprecationWarnings — must be explicitly expected by a test.
- `tests/conftest.py` defines an autouse `leak_detector` that pops every still-pushed `app_ctx` after each test and asserts the leak list is empty — a hard guard against context-management regressions.
- The autouse `_standard_os_environ` fixture monkeypatch-deletes `FLASK_ENV_FILE`, `FLASK_APP`, `FLASK_DEBUG`, `FLASK_RUN_FROM_CLI` and `WERKZEUG_RUN_MAIN` so CLI tests can't leak environment state into each other.
- The shared `app` fixture builds `Flask("flask_test", root_path=os.path.dirname(__file__))` with `TESTING=True` and `SECRET_KEY="test key"`, which is why `tests/templates/` and `tests/static/` resolve as the app's template and static folders.
- Test weight is concentrated: `test_basic.py` 1974 lines / 95 test functions, `test_blueprints.py` 1115, `test_cli.py` 703 / 34 tests, `test_templating.py` 532 — versus 28 lines for `test_session_interface.py` and 21 for `test_subclassing.py`.
- CLI tests are backed by fixture apps under `tests/test_apps/`, including `cliapp/factory.py`, `cliapp/multiapp.py`, `cliapp/importerrorapp.py`, a nested `inner1/inner2/flask.py` shadowing case, and `helloworld/wsgi.py` for implicit discovery — plus checked-in `.env` and `.flaskenv` files.
- Async coverage is gated on the optional extra: `tests/test_async.py` begins with `pytest.importorskip("asgiref")` and exercises async views, async `View.dispatch_request`, async `MethodView` handlers and async error handlers across app and blueprint.
- Type checking is a first-class test target: `tests/type_check/` contains `typing_route.py`, `typing_app_decorators.py` and `typing_error_handler.py`, and both mypy (`strict = true`, `files = ["src", "tests/type_check"]`) and pyright run over them in the `typing` tox env and a dedicated CI job.
- The test matrix is unusually broad: CPython 3.10–3.15 including free-threaded `3.14t`, Windows and macOS runners, PyPy 3.11, a minimum-dependency env pinning exact lower bounds, and a dev env installing six Pallets libraries from git main tarballs.
- Coverage is configured (branch mode, `source = ["flask", "tests"]`, `exclude_also` for TYPE_CHECKING blocks and `raise NotImplementedError`) but no workflow runs coverage or enforces a threshold — the tox commands invoke plain `pytest -v --tb=short`.
- Example apps carry their own tests: `examples/tutorial/tests/` has four modules (auth, blog, db, factory) plus `data.sql` seed data, and `examples/javascript/tests/test_js_example.py` exists — but neither is in `testpaths`, so CI never runs them.
- Session security behaviour has direct tests — SameSite attribute variants and `SECRET_KEY_FALLBACKS` key rotation are asserted in `test_basic.py` around lines 342–420.
Risk map
Flask's security-sensitive surface is narrow but load-bearing: signed session cookies, host validation, file serving, template autoescaping, and code that executes user-supplied Python. The largest structural risk in this checkout is the 3.2 context merge and the `ctx`-first-parameter signature change, which is backstopped only by a signature-sniffing `__init_subclass__` shim that mutates the base `Flask` class at subclass-definition time. Config loading (`from_pyfile`), the shell command (`PYTHONSTARTUP`), and CLI app discovery (`--cert` import strings, factory `ast.literal_eval`) all evaluate code or import arbitrary modules by design. Several defaults are permissive by intent and must be set by deployers: `SESSION_COOKIE_SECURE=False`, `SESSION_COOKIE_SAMESITE=None`, `SECRET_KEY=None`, `TRUSTED_HOSTS=None`.
- `Flask.__init_subclass__` mutates global class state: when it detects an old-signature override it calls `setattr(Flask, method.__name__, add_ctx(base_method))` on the *base* class, so one legacy subclass anywhere in the process rewraps `Flask`'s methods for every app in that process.
- `Config.from_pyfile` compiles and `exec`s the target file into a synthetic module namespace — arbitrary code execution by design, so config paths must never come from untrusted input.
- `flask shell` executes `PYTHONSTARTUP` via `eval(compile(f.read(), startup, "exec"), ctx)` before the REPL starts, inheriting whatever that env var points at.
- `CertParamType.convert` falls back to `werkzeug.utils.import_string(value, silent=True)` for `--cert`, so a non-path `--cert` value imports an arbitrary dotted module attribute looking for an `ssl.SSLContext`.
- `find_app_by_string` parses `module:factory(args)` with `ast.parse` and `ast.literal_eval` — literal-only, so it resists arbitrary expression injection — but `locate_app` still `__import__`s the module, executing it fully.
- `open_resource` and `open_instance_resource` join user-supplied `resource` onto `root_path`/`instance_path` with plain `os.path.join` and no `safe_join`, so an absolute or `..` path escapes the directory — safe for the intended developer-controlled use, unsafe if fed request data.
- By contrast `send_from_directory` explicitly documents and delegates to Werkzeug's `safe_join`, and the docstring warns that the `directory` argument itself must never come from the client.
- Security-relevant defaults ship permissive: `SECRET_KEY=None`, `SECRET_KEY_FALLBACKS=None`, `SESSION_COOKIE_SECURE=False`, `SESSION_COOKIE_SAMESITE=None`, `TRUSTED_HOSTS=None`, `MAX_CONTENT_LENGTH=None` — with no secret key, `get_signing_serializer` returns None and sessions silently degrade to `NullSession`.
- Session cookie signing defaults to SHA-1 HMAC via `_lazy_sha1`, deferred to runtime specifically so FIPS builds without SHA-1 can fail at configure time rather than import time.
- `open_session` swallows `BadSignature` and returns an empty session, so tampered or key-rotated-out cookies are indistinguishable from no cookie at the application layer — no signal is emitted.
- Teardown error handling changed to collect-then-raise: `_CollectErrors.raise_any` raises a `BaseExceptionGroup` on Python 3.11+ but only `self.errors[0]` on 3.10, so identical failures surface as different exception types across supported versions.
- Dead/stale developer config: `.devcontainer/on-create-command.sh` runs `pip install -r requirements/dev.txt`, but no `requirements/` directory exists — the repo moved to uv dependency groups in `pyproject.toml`, so devcontainer creation fails.
Extension points
Flask is built almost entirely out of swap points. Class-level attributes on `App`/`Flask` (`request_class`, `response_class`, `json_provider_class`, `config_class`, `app_ctx_globals_class`, `url_rule_class`, `jinja_environment`, `aborter_class`, `session_interface`, `test_client_class`) let subclasses replace nearly every collaborator. Registration flows through `@setupmethod`-guarded decorators that append into dicts keyed by blueprint name or `None`. Third-party CLI commands load from the `flask.commands` entry point group; extensions stash state in `app.extensions[name]`. The JSON layer has two independent extension seams: `JSONProvider` for request/response JSON and `JSONTag`/`TaggedJSONSerializer.register` for session payload types.
- Swappable classes are declared as plain class attributes and instantiated lazily: `aborter_class`, `jinja_environment`, `app_ctx_globals_class`, `config_class`, `json_provider_class`, `url_rule_class`, `url_map_class`, `test_client_class`, `test_cli_runner_class`.
- Hook factories are explicitly designed for override: `make_config`, `make_aborter`, `create_jinja_environment`, `create_global_jinja_loader`, `create_url_adapter`, `auto_find_instance_path`, `select_jinja_autoescape`, `make_shell_context`, `ensure_sync`, `async_to_sync`.
- Third-party CLI commands plug in through the `flask.commands` entry point group, loaded once by `FlaskGroup._load_plugin_commands` before any built-in or app command lookup.
- Extensions store per-app state in `app.extensions`, keyed by the extension module's short name — the celery example demonstrates the convention with `app.extensions["celery"] = celery_app`.
- JSON behaviour is swapped by subclassing `JSONProvider` (implement `dumps`/`loads`; `dump`, `load` and `response` have defaults) and setting `json_provider_class` or assigning `app.json` directly.
- Session payload types extend via `JSONTag` subclasses registered with `TaggedJSONSerializer.register(tag, force=..., index=...)`; the module docstring walks through adding an `OrderedDict` tag at index 0.
- Session storage backends plug in by subclassing `SessionInterface` (implementing `open_session`/`save_session`, which raise `NotImplementedError` in the base) and assigning `app.session_interface`; `null_session_class` and `should_set_cookie` are also override points.
- The registries new handlers append into are all name-keyed dicts on `Scaffold`/`App`: `view_functions`, `error_handler_spec`, `before_request_funcs`, `after_request_funcs`, `teardown_request_funcs`, `url_value_preprocessors`, `url_default_functions`, `template_context_processors`, with `None` meaning app-level.
- `@setupmethod` wraps every registration decorator with `_check_setup_finished`, which raises an `AssertionError` once `_got_first_request` is true — so extensions must complete registration before the first request.
- Custom CLI groups are supported end-to-end: `AppGroup.command` auto-wraps callbacks in `with_appcontext` (disable with `with_appcontext=False`) and `AppGroup.group` defaults nested groups to `AppGroup`; `FlaskGroup(create_app=...)` builds a bespoke script entrypoint.
- Blueprint CLI grouping is a three-way switch in `Blueprint.register`: `cli_group=None` merges commands into `app.cli` flat, the `_sentinel` default nests them under the blueprint name, and an explicit string nests them under that name.
- Template extension points: `template_filter`, `template_test`, `template_global` (usable with or without parentheses as of 3.2) on the app, mirrored by `app_template_filter`/`app_template_test`/`app_template_global` on blueprints, plus `DispatchingJinjaLoader._iter_loaders` for custom template discovery.
Contributor handoff
Read the sans-IO README first — it defines the hardest rule in this repo: nothing in `src/flask/sansio/` may do IO or touch the Flask globals, so context-dependent code belongs in `app.py`/`blueprints.py`. This is the 3.2 development line, so the second thing to internalise is that `RequestContext` no longer exists as a distinct class and that dispatch-related `Flask` methods take `ctx: AppContext` as their first parameter. Every user-visible change needs a `CHANGES.rst` entry with a `:pr:`/`:issue:` reference and a `.. versionadded:: 3.2` / `.. versionchanged:: 3.2` directive in the docstring — this is followed consistently throughout the codebase. Verify with `tox run -e py3.13` (or plain `pytest`), `tox run -e typing`, and `tox run -e style`; `filterwarnings = error` means a new warning without a matching test will break the suite.
- Read first, in order: `src/flask/sansio/README.md` (the IO boundary rule), `src/flask/ctx.py` (the merged `AppContext`), `src/flask/app.py` `wsgi_app`/`full_dispatch_request` (the dispatch spine), and `docs/lifecycle.rst` (the maintainers' own narrative of setup → serving → request).
- Safe edit boundary: code in `src/flask/sansio/` must not import `flask.globals` or perform IO; if a change needs `current_app`, `request` or the filesystem, it belongs in the IO subclass in `src/flask/app.py` or `src/flask/blueprints.py`.
- Trap: if you add or change a `Flask` dispatch method, it must take `ctx: AppContext` as the first parameter after `self`, or `__init_subclass__`'s signature sniffing will treat subclass overrides as legacy and silently rewrap base-class methods.
- Trap: `filterwarnings = ["error"]` turns every warning into a test failure — any new `DeprecationWarning` must be paired with a test that expects it.
- Convention: every public change gets a `CHANGES.rst` bullet under the unreleased version with a `:pr:` or `:issue:` role, as the 3.2.0 section demonstrates for all 13 current entries.
- Convention: docstrings carry Sphinx `.. versionadded::` / `.. versionchanged::` directives naming the exact release — e.g. `select_jinja_autoescape` documents the 3.2 case-insensitivity change and `do_teardown_request` documents the 3.2 all-callbacks change.
- Tests to run: `pytest` (or `tox run` for a specific Python), `tox run -e typing` for mypy+pyright, and `tox run -e style` for the full pre-commit set; docs changes need `tox run -e docs`, which builds with `-W` so any Sphinx warning fails.
- Style is enforced by ruff with `select = ["B", "E", "F", "I", "UP", "W"]` and isort configured `force-single-line = true`, `order-by-type = false` — every import is on its own line throughout `src/`, matching that setting.
- Typing is strict and non-optional: `[tool.mypy] strict = true` over `src` and `tests/type_check`, plus pyright in basic mode; the package ships `src/flask/py.typed`, so any missing annotation is a public-API regression.
- Shared return/callback types live in `src/flask/typing.py` (`ResponseReturnValue`, `ResponseValue`, `HeadersValue`, `AppOrBlueprintKey`); extend these rather than inlining unions, and note `ResponseValue` deliberately includes `AsyncIterable` for Quart's benefit.
- Trap: `Blueprint.get_send_file_max_age` and `Blueprint.send_static_file` duplicate the `Flask` versions by design — a fix in one must be mirrored in the other.
- Trap: registration methods are frozen after the first request by `@setupmethod`/`_check_setup_finished`, so any new registration API must be decorated with `@setupmethod` to stay consistent.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 83 | 91% |
| HTML | 20 | docs/config |
| YAML | 8 | docs/config |
| Markdown | 6 | docs/config |
| TOML | 5 | 5% |
| CSS | 2 | docs/config |
| JSON | 2 | docs/config |
| SQL | 2 | 2% |
Start reading here
Probable source entrypoint: pyproject.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
src/ (26 files)
Source layout — src/
| Path | Files |
|---|---|
src/flask/ | 26 |
Repository structure
| Top-level path | Files |
|---|---|
docs/ | 82 |
tests/ | 61 |
examples/ | 42 |
src/ | 26 |
.github/ | 9 |
.devcontainer/ | 2 |
Docs found: README.md
Package
pyproject.toml — flask v3.2.0.dev, 2 declared dependencies.
asyncdotenv
CI & config entrypoints
README.md— Repository readme
Structural health signals
Average score 7.5/10 across 35 analyzed source files; 8 hotspots surfaced.
src/flask/app.py— 1222 lines, score 5.7 (Large file, Deep nesting)src/flask/cli.py— 1127 lines, score 5.7 (Large file, Deep nesting)src/flask/sansio/app.py— 1013 lines, score 5.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
d73fa1cdcbd82026-09-08 — fix docs refd318b68347112026-08-16 — explain seek2a8a38b051fc2026-08-11 — support query in methodviewd8eaaba824652026-08-11 — add `app.query` route decorator (#6133)89992954ec712026-08-11 — add app.query route decorator