flasklet
A few nice things for Flask: typed application and request state, flash messages with delivery types, convention-based blueprint discovery, route imports, and template rendering helpers.
Quick start
Install with uv add flasklet. Create these files:
myapp/
__init__.py
state.py
admin/
__init__.py
routes.py
templates/
index.html
components.html
partial.html
myapp/state.py:
from enum import StrEnum
from typing import Self
from flask import request
from flasklet import FlaskAppState, FlaskRequestState, TypedFlask, config
class Mode(StrEnum):
DEVELOPMENT = "development"
PRODUCTION = "production"
class AppState(FlaskAppState, prefix="MYAPP_"):
secret_key: str = config.from_env(in_app_config=True, secret=True)
mode: Mode = config.from_env(default=Mode.DEVELOPMENT)
class RequestState(FlaskRequestState):
is_htmx: bool
@classmethod
def load(cls) -> Self:
return cls(is_htmx=request.headers.get("hx-request") == "true")
current_app = TypedFlask.current_app(AppState, RequestState)
myapp/__init__.py:
from flasklet import TypedFlask, init_template_helpers, register_blueprints
from myapp.state import AppState, RequestState
def create_app() -> TypedFlask[AppState, RequestState]:
app = TypedFlask(__name__, app_state=AppState.load_from_env(), request_state=RequestState)
init_template_helpers(app)
register_blueprints(app, auto_import_routes=True)
return app
myapp/admin/__init__.py:
from flask import Blueprint
bp = Blueprint("admin", __name__)
myapp/admin/routes.py:
from flask import Response, redirect, render_template, url_for
from flasklet import flash_message, parse_flashed_messages
from myapp.state import current_app
from . import bp
@bp.get("/")
def index() -> str:
return render_template(
"index.html",
mode=current_app.app_state.mode.value,
messages=parse_flashed_messages(),
)
@bp.post("/notice")
def notice() -> Response:
flash_message("Saved", type="toast", category="success")
return redirect(url_for("admin.index"))
myapp/templates/index.html:
{{ render_macro("components.html", "badge", value=mode) }}
{{ render_block("components.html", "notice", text="Welcome") }}
{{ render_partial("partial.html", text="Rendered separately") }}
{% for message in messages %}
<p data-type="{{ message.type }}" class="{{ message.category }}">{{ message.text }}</p>
{% endfor %}
<form action="{{ url_for('admin.notice') }}" method="post">
<button type="submit">Flash a message</button>
</form>
myapp/templates/components.html:
{% macro badge(value) %}<strong>{{ value }}</strong>{% endmacro %}
{% block notice %}<p>{{ text }}</p>{% endblock %}
myapp/templates/partial.html:
<aside>{{ text }}</aside>
Run MYAPP_SECRET_KEY=local-example-only uv run flask --app myapp:create_app run,
then open http://127.0.0.1:5000/.
Typed application and request state
Flask gives you untyped places for state: app.config for values set at
startup and flask.g for values of the current request. Both accept any key
and return Any, so typos and wrong types surface at runtime. TypedFlask
replaces them with two classes you define:
app_state: an instance of aFlaskAppStatesubclass. It is frozen, created once at startup, and doubles as the app's configuration.request_state: aFlaskRequestStatesubclass.TypedFlaskcalls itsloadclassmethod at the start of every request to create a fresh, mutable instance.
Both base classes turn subclasses into keyword-only dataclasses. The type
checker reports an error at TypedFlask(...) if the request state class has
no load, or if load returns something else. The state types are inferred
from the constructor arguments, so app is a
TypedFlask[AppState, RequestState]. Omit request_state if
the app needs no request state; the app is then a
TypedFlask[AppState, NoRequestState], its typed reference is
TypedFlask.current_app(AppState), and reading request_state is a type error.
Other Flask constructor options are passed as
keywords, and Flask's g keeps working for extensions.
Application state and configuration
FlaskAppState is also the app's configuration. Declare fields with the
helpers in flasklet.config:
from pathlib import Path
class AppState(FlaskAppState, prefix="MYAPP_"):
secret_key: str = config.from_env(in_app_config=True, secret=True)
mode: Mode = config.from_env(default=Mode.DEVELOPMENT)
data_dir: Path | None = config.from_env(default=None)
base_dir: Path = config.derived(lambda s: (s.data_dir or Path.cwd()).resolve())
session_cookie_samesite: str = config.constant(default="Lax", in_app_config=True)
app = TypedFlask(__name__, app_state=AppState.load_from_env())
config.from_envbinds a field to an environment variable.config.derivedcomputes a field from the others, once, in declaration order.config.constantis a fixed value; a subclass may redeclare it.in_app_config=Truealso copies the value intoapp.configwhen the app is created, under the uppercased field name:secret_keybecomesSECRET_KEY. Use it for keys that Flask and extensions read, and name the field after the key they expect. Your own code readsapp_state, neverapp.config.secret=Trueleaves the value out of the repr.
load_from_env() reads each from_env field from its variable: the class
prefix plus the uppercased field name, or the name passed to from_env.
load_from_env(prefix=...) adds a prefix in front of the class prefix. A
field without a default is required.
Supported types are str, int, float, bool, Path, Enum subclasses,
Optional[T], list[T], and NewType wrappers. Enum variables match the
string representation of a member's value exactly. For example,
MYAPP_MODE=production selects Mode.PRODUCTION. An invalid value reports the
environment variable and allowed values. Booleans accept only true and
false. Lists are comma-separated; an empty variable gives an empty list. For
Optional[T], an empty variable gives None. Mutable defaults are
deep-copied.
Because you create the instance, a test or another deployment can build a
different one. dataclasses.replace gives a changed copy, with derived fields
computed again. A subclass adds fields, for example Flask keys only tests need:
class TestAppState(AppState):
testing: bool = config.constant(default=True, in_app_config=True)
state = replace(TestAppState(secret_key="test"), mode=Mode.PRODUCTION)
app = TypedFlask(__name__, app_state=state)
Using the app from other modules
Flask's current_app is typed as a plain Flask, so it knows nothing about
your state. Create a typed reference once, next to your state classes, as in
myapp/state.py above, and import it from there. Creating the reference does
not need an app. Using it needs an active application context, and checks that
the active app's state types match (subclasses are accepted).
Common Flask patterns
Per-request values. In Flask you set attributes on g in a
before_request hook and read them with g.get("name", default). With
flasklet, load builds the whole request state at once:
# Flask
@app.before_request
def load_user() -> None:
g.user_id = authenticate(request)
g.is_htmx = request.headers.get("hx-request") == "true"
user_id = g.get("user_id") # Any
# flasklet
class RequestState(FlaskRequestState):
user_id: int | None
is_htmx: bool
error_status: int | None = None
@classmethod
def load(cls) -> Self:
return cls(
user_id=authenticate(request),
is_htmx=request.headers.get("hx-request") == "true",
)
user_id = current_app.request_state.user_id # int | None
Fields need no placeholder defaults, and a forgotten field is a type error.
Give defaults only to fields that code changes later in the request, such as
error_status.
Configuration values. In Flask you read current_app.config["KEY"]. With
flasklet, the value is a typed field of AppState, converted to its real type
(enums, paths, numbers):
# Flask
if current_app.config["ENVIRONMENT"] == "production":
...
# flasklet
if current_app.app_state.mode is Mode.PRODUCTION:
...
Timing and cleanup. RequestState.load runs at the start of each request,
so started_ns=time.time_ns() replaces a before_request hook that sets
g.start. request_state stays available in the teardown callbacks, so
resources opened for a request can be closed there:
@app.teardown_request
def close_connection(_exc: BaseException | None) -> None:
if (conn := current_app.request_state.connection) is not None:
conn.close()
Neither teardown_request nor teardown_appcontext runs at application
shutdown. Resources shared by the whole app (connection pools, clients) belong
to their owner and are closed at shutdown. TypedFlask closes nothing
automatically.
Lifetime
The TypedFlask constructor copies the app state's in_app_config fields into
app.config before any extension is initialized, so extensions see them.
RequestState.load is registered in the constructor, so it runs before every
other before_request function, including those of extensions and
blueprints. Its result is stored on g. The state is then available in all
later before_request functions, views, error handlers, after_request,
teardown_request, and teardown_appcontext. URL value preprocessors run
before before_request, so they cannot read it.
Outside a request, such as in CLI commands and bare app.app_context()
blocks, reading request_state raises RuntimeError.
app.test_request_context() does not run before_request either; call
app.preprocess_request() inside it to load the state:
with app.test_request_context("/?user=3"):
app.preprocess_request()
assert app.request_state.user_id == 3
If an app context is already active when a request starts, as in a test that
pushes one around the test client, Flask reuses it and its g. load still
runs for every request, so each request sees fresh state. After the request,
the outer context keeps the state of the last request.
If RequestState.load raises, the request fails like any other unhandled
exception. Error handlers that read request_state then raise as well.
See the runnable example.
Blueprint and route discovery
register_blueprints(app) scans subpackages of the app package for a module
attribute named bp. It registers each discovered blueprint on its parent
blueprint or on the app. A subpackage without bp and its children are skipped.
Call it during app creation, before handling requests. The same imported
blueprints can be registered on separate app instances.
Pass auto_import_routes=True to import modules next to each discovered blueprint
and in its routes/ directory before registration. This is opt-in because
importing a module runs its top-level code. The quick start uses this option.
import_routes(__file__, __name__) imports every Python module in the current
package directory and its routes/ directory, in sorted order. Route
decorators run as those modules are imported. It returns None. Use it in a
package's __init__.py when you want explicit route imports instead.
Flash messages
flash_message(text, type="toast", category="success") adds a delivery type to
Flask's ordinary flash category. Read messages with parse_flashed_messages();
each result has text, type, and category. Messages from Flask's own
flash() are returned with type="message". Use parse_flashed_messages()
when mixing the two functions.
Template helpers
init_template_helpers(app) registers render_block, render_macro,
render_macro_unsafe, and render_partial as Jinja globals. Call it in the
app factory before rendering templates. You can also register individual
functions with app.add_template_global.
render_partial renders a complete template. render_block renders a block
defined in the named template; it does not resolve a block that exists only in
a parent template, and the selected block cannot use super().
render_macro renders only the selected macro. Other top-level expressions in
the source template are not evaluated. Its keyword arguments serve two roles:
arguments with names declared by the macro are passed to it, and all arguments
are available as template context. A misspelled or unused keyword argument
has no effect, so check argument names when output is missing.
render_macro follows the app's Jinja autoescaping. render_macro_unsafe disables
autoescaping and returns an ordinary string. Use it only when the macro values
are trusted HTML. When calling it from a Jinja template, the result needs
|safe to avoid escaping the rendered HTML.
To include a file's source without Jinja processing, import
IncludeRawExtension from flasklet, register it with
app.jinja_env.add_extension(IncludeRawExtension), and use
{% include_raw "path/to/file" %} in a template.
Metadata
Release files for flasklet 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| flasklet-2.0.1.tar.gz | 17.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flasklet-2.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.9 kB
Release files / flasklet-2.0.1.tar.gz
| Download URL | flasklet-2.0.1.tar.gz |
|---|---|
| Size | 17.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0693636540502f7ffcddd30ed3beeed32535528a309c30125bc9f1fb1d21ea1f
|
|
BLAKE2b-256 checksum How to use checksums |
7ca406be0728d13a7fa23d73c578320955937711bc6b1ff33cdf197fb10aece8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / flasklet-2.0.1-py3-none-any.whl
| Download URL | flasklet-2.0.1-py3-none-any.whl |
|---|---|
| Size | 21.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e2a1f25212f504a7d397fa724442d50a1026fda1a186d9a740a7d417534a7330
|
|
BLAKE2b-256 checksum How to use checksums |
bfcfb298dc4fcbfc008502b65c400be80ce29f95239e957fa5861d8fe58239ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|