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.
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| litestar_settings-0.1.0.tar.gz | 65.1 kB | Without provenance | Details for litestar_settings-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| litestar_settings-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for litestar_settings-0.1.0-py3-none-any.whl |
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}
|
Security for litestar-settings 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.