litestar-settings
Typed settings for Litestar apps, built on msgspec.Struct - like
pydantic-settings, but msgspec instead of Pydantic.
Features
- Framework-agnostic core.
litestar_settings.corehas no dependency on Litestar; a settings schema is just amsgspec.Struct. - Composable sources.
EnvSource,DotEnvSource,TomlSource,YamlSource,SecretsDirSource,MappingSource- each reads from one place and returns a raw nesteddict. - Explicit merge order. You pass
load_settings(cls, sources)an ordered list; later sources override earlier ones, recursively for nested dicts. No hidden "env beats file beats default" precedence. - One validation pass. All sources are merged into a single raw mapping first; that mapping is
converted into your
Structexactly once viamsgspec.convert(). Secret[T]fields. Wrap secret-bearing fields (api_key: Secret[str]) so they never leak intorepr()/logs by accident - built automatically, nodec_hookwiring needed.- Thin Litestar plugin.
SettingsPluginbuilds theStructonce, exposes it through DI, and fails app boot - not the first request - if the merged config doesn't satisfy the schema. - No hot-reload. Settings load once, at startup - see Why no hot-reload.
Install
pip install litestar-settings # msgspec + python-dotenv, incl. DotEnvSource
pip install 'litestar-settings[yaml]' # adds YamlSource (ruamel.yaml)
pip install 'litestar-settings[litestar]' # adds the Litestar plugin's dependency
TOML support needs no extra - it uses the stdlib tomllib (Python 3.11+).
Quick start
import msgspec
from litestar_settings import DotEnvSource, EnvSource, MappingSource, TomlSource, load_settings
class DBSettings(msgspec.Struct):
host: str
port: int = 5432
class Settings(msgspec.Struct, kw_only=True):
debug: bool = False
db: DBSettings
settings = load_settings(
Settings,
[
TomlSource("settings.toml"), # 1. defaults / base config
DotEnvSource(".env", prefix="APP_"), # 2. local dev overrides
EnvSource(prefix="APP_"), # 3. real environment wins
MappingSource({"debug": True}), # 4. explicit override, e.g. from a CLI flag
],
)
Sources are applied left to right; each later source deep-merges onto the result of the ones
before it. Nested keys work through the same __ convention across EnvSource, DotEnvSource,
and SecretsDirSource:
APP_DEBUG=true
APP_DB__HOST=localhost
APP_DB__PORT=6543
becomes {"debug": "true", "db": {"host": "localhost", "port": "6543"}}, which
load_settings then converts (lax by default, so "6543" becomes 6543: int).
Pass strict=True to load_settings to disable that coercion and require exact types.
Usage
Secrets files (Docker/Kubernetes style)
from litestar_settings import SecretsDirSource
# /run/secrets/db__password -> {"db": {"password": "<file contents>"}}
SecretsDirSource("/run/secrets")
Secret fields
msgspec.Struct's default repr() prints every field verbatim - including secrets. Wrap
secret-bearing fields in Secret[T] so they don't end up in logs or tracebacks by accident:
import msgspec
from litestar_settings import Secret, load_settings, TomlSource
class Settings(msgspec.Struct, kw_only=True):
api_key: Secret[str]
db_port: Secret[int] = Secret(5432)
settings = load_settings(Settings, [TomlSource("settings.toml")])
print(settings) # Settings(api_key=Secret('**********'), db_port=Secret('**********'))
settings.api_key.get_secret_value() # the real value
load_settings() builds Secret[...] fields automatically - the wrapped type (str, int, ...)
still goes through the normal msgspec.convert() coercion, so Secret[int] from an env var string
works the same as a plain int field would. No dec_hook wiring needed; pass your own dec_hook
for other custom types and it's consulted after Secret.
Secret only guards repr()/str() - it doesn't stop you from serializing the wrapped value
elsewhere (e.g. msgspec.json.encode(settings.api_key.get_secret_value())), and encoding a
Secret-typed field back to JSON needs its own enc_hook since msgspec doesn't know the type.
Custom sources
Source is a structural Protocol - any object with a zero-arg load() method returning a raw
nested mapping satisfies it, no subclassing required:
from litestar_settings import RawMapping
class RedisSource:
def __init__(self, client: Redis, key: str) -> None:
self._client = client
self._key = key
def load(self) -> RawMapping:
return json.loads(self._client.get(self._key) or "{}")
RawMapping is just dict[str, object], exported for readability - using the plain dict[str, object] annotation works identically.
Litestar usage
from __future__ import annotations
import msgspec
from litestar import Litestar, get
from litestar.config.app import AppConfig
from litestar.di import NamedDependency
from litestar_settings import EnvSource, TomlSource
from litestar_settings.plugin import SettingsPlugin
class Settings(msgspec.Struct, kw_only=True):
debug: bool = False
api_key: str
def apply_app_config(settings: Settings, app_config: AppConfig) -> AppConfig:
app_config.debug = settings.debug
return app_config
settings_plugin = SettingsPlugin(
Settings,
[TomlSource("settings.toml"), EnvSource(prefix="APP_")],
apply_app_config=apply_app_config, # optional - see below
)
@get("/status")
async def status(settings: NamedDependency[Settings]) -> dict:
return {"debug": settings.debug}
app = Litestar(route_handlers=[status], plugins=[settings_plugin])
Fail-fast timing
By default SettingsPlugin builds Settings lazily behind a cached factory and forces that
first build from an on_startup hook - so a missing api_key fails ASGI lifespan startup
(litestar run, AsyncTestClient(app=app), ...), not the first request that happens to depend
on settings. Constructing Litestar(...) itself never fails on bad config; importing the app
module stays cheap even if the environment isn't fully set up yet (useful for litestar routes,
schema generation, etc.).
Pass apply_app_config when you want native AppConfig fields (debug, openapi_config, ...)
derived from Settings. Since AppConfig must be finalized synchronously inside on_app_init,
this switches the plugin to build Settings eagerly at that point instead of at on_startup -
still fail-fast, just earlier (at Litestar(...) construction time).
Custom dependency key
SettingsPlugin(Settings, [...], dependency_key="app_settings")
@get("/status")
async def status(app_settings: NamedDependency[Settings]) -> dict: ...
Design
Core is framework-agnostic. litestar_settings.core has no dependency on Litestar and no
magic layered on top of typing. A settings schema is just a msgspec.Struct. Its only required
dependencies are msgspec and python-dotenv (for DotEnvSource); YamlSource and the Litestar
plugin are behind extras - see Install.
Sources are plain objects with a load() method. EnvSource, DotEnvSource, TomlSource,
YamlSource, SecretsDirSource, MappingSource - each reads from one place and returns a raw
nested dict. None of them validate or coerce anything.
Merge order is an explicit list, not hidden precedence. You pass load_settings(cls, sources)
an ordered list; later sources override earlier ones, recursively for nested dicts. There is no
built-in "env beats file beats default" rule baked into the source types themselves.
One convert() pass at the end. All sources are merged into a single raw mapping first; that
mapping is converted into your Struct exactly once via msgspec.convert(). Sources never touch
msgspec at all.
Secret[T] is built into that one pass. load_settings() composes a dec_hook that
recognizes Secret[...] field types and wraps them automatically; a dec_hook you pass yourself
is consulted for anything Secret doesn't handle. See Secret fields.
Litestar layer is a thin wrapper on top. SettingsPlugin builds the Struct once, exposes it
through DI (Provide(get_settings, use_cache=True)), and fails app boot - not the first request -
if the merged config doesn't satisfy the schema.
Load-once at startup, no hot-reload. See Why no hot-reload.
Why no hot-reload
Settings load once, at startup. There's no file watcher, no signal handler, no live-reload path.
- Predictability. If config could change under a running process, two reads of
settings.db_urlwithin the same request could theoretically disagree. That's a class of bug worth not having. - Fail-fast stays simple. With validation only at startup, the service either boots with a valid config or doesn't boot. Hot-reload forces a decision about what to do with an invalid config arriving mid-flight (roll back? crash? ignore the bad value?) - a separate problem this library doesn't try to solve.
- Most settings don't need it. DB URLs, secret keys, uptime-scoped feature flags - a restart
(
systemd restart, a new container) is cheap and makes the reload story trivial.
If you need live-updated values (e.g. a feature flag toggled without a restart), that's a different problem - a small polling/pubsub-backed accessor - and deliberately out of scope here.
Development
uv sync --all-extras
uv run ruff check
uv run ruff format --check
uv run pyrefly check
uv run pyrefly coverage check
uv run pytest
License
BSD 3-Clause. See LICENSE.
Release files for litestar-settings 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| litestar_settings-0.1.0.tar.gz | 65.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| litestar_settings-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.1 kB
Release files / litestar_settings-0.1.0.tar.gz
| Download URL | litestar_settings-0.1.0.tar.gz |
|---|---|
| Size | 65.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
51b15f45e23e375d37fce37d3ae50b9d09842e0082c4cf0ae15354e30a2b0c65
|
|
BLAKE2b-256 checksum How to use checksums |
0764aef3091efb427112a0367d8077d44a0a85330b264fe41a16ec682c88a828
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / litestar_settings-0.1.0-py3-none-any.whl
| Download URL | litestar_settings-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a9ee1e7e110d9a9a6cb1933c215446f209c871bf93b63fe96129eedda0eb68b7
|
|
BLAKE2b-256 checksum How to use checksums |
48f1ac551277542cf6159c617153bffbf290b89a6d6f1f7fcfa9b28fa4199e56
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|