A Settings library using msgspec as a backend for validation and serialization.
Project description
msgspec-config
Typed, multi-source configuration loading on top of msgspec.
msgspec-config is for applications that need:
- one typed model for configuration shape
- multiple config inputs (files,
.env, environment, CLI, custom providers) - deterministic precedence across all inputs
- strict validation/coercion without writing parsing glue
The core idea is simple: define one DataModel, attach ordered DataSources, and instantiate the model.
API Docs
Please visit the API docs at this project's github pages site: https://maxpareschi.github.io/msgspec-config/
Installation
pip install msgspec-config
uv add msgspec-config
Tested on Python>=3.11
Quick Start (Layered Config)
config.toml:
host = "toml-host"
port = 7000
[log]
level = "INFO"
.env:
APP_PORT=7500
APP_LOG_LEVEL=DEBUG
from msgspec_config import (
APISource,
CliSource,
DataModel,
DotEnvSource,
EnvironSource,
JSONSource,
TomlSource,
datasources,
entry,
group,
)
class LogConfig(DataModel):
level: str = "WARN"
file_path: str = "/var/log/app.log"
@datasources(
TomlSource(toml_path="config.toml"),
DotEnvSource(dotenv_path=".env", env_prefix="APP"),
EnvironSource(env_prefix="APP"),
CliSource(),
)
class AppConfig(DataModel):
host: str = entry("127.0.0.1", min_length=1)
port: int = entry(8080, ge=1, le=65535)
debug: bool = False
log: LogConfig = group(collapsed=True)
cfg = AppConfig(port=9000)
print(cfg.model_dump_json(indent=2))
Precedence is deterministic and intentional:
defaults < source_1 < source_2 < ... < source_n < kwargs
With the example above:
- model defaults are the baseline
TomlSourceoverrides defaultsDotEnvSourceoverrides TOMLEnvironSourceoverrides.envCliSourceoverrides environment values- constructor kwargs (
AppConfig(port=9000)) win last
Rationale: this gives safe defaults in code, then progressive override points for deploy/runtime, while still keeping a final explicit override path in Python.
Important:
env_prefixis mandatory for bothEnvironSourceandDotEnvSource.- Empty/blank prefixes raise
ValueError.
Field Helpers (entry and group)
entry(...)
Use entry(...) when you need validation metadata and/or safe mutable defaults.
Why it exists:
- attaches
msgspec.Meta(...)constraints directly from field declaration - converts mutable defaults (
list,dict,set) into factories automatically - supports extra UI/schema keys:
hidden_if,disabled_if,parent_group,ui_component - supports CLI include/exclude key:
cli(Trueinclude,Falseexclude) - supports CLI override keys:
cli_flag,cli_short_flag
from msgspec_config import DataModel, entry
class ApiConfig(DataModel):
timeout_seconds: int = entry(30, ge=1, le=120, description="Request timeout")
tags: list[str] = entry([], description="Dynamic tags")
group(...)
Use group(...) for nested object/list/dict fields inferred from annotations.
Why it exists:
- creates safe defaults for nested structures without shared state
- adds optional UI/schema hints (
collapsed,mutable) - accepts direct metadata kwargs without
Annotated[...]msgspec.Metakwargs are passed through- arbitrary extra keys are stored under
Meta.extra_json_schema
from msgspec_config import DataModel, group
class Child(DataModel):
value: int = 1
class Parent(DataModel):
child: Child = group(collapsed=True)
children: list[Child] = group(mutable=True)
by_name: dict[str, Child] = group(mutable=True)
server: Child = group(cli_short_flag="sv", ui_component="object-editor")
Notes:
- object annotations used with
group()must be zero-arg constructible group()is for object/list/dict-like fields, not primitive scalars
Built-in Sources (Behavior)
All built-ins are importable from both msgspec_config and msgspec_config.sources.
When a source is used with resolve(model=...) (or through @datasources(...) on a
DataModel), field resolution accepts both canonical and encoded/alias names, and mapped
output keys are emitted using encoded names.
TomlSource and YamlSource
- load mappings from files using
msgspec.toml.decode/msgspec.yaml.decode - if path is unset or missing, they return
{}(treated as "source absent") - parse/read failures raise
RuntimeErrorwith file context
JSONSource
- decodes inline JSON (
json_data) or loads JSON fromjson_path - if both are set,
json_datatakes precedence - if path is unset/missing, returns
{} - parse/read failures raise
RuntimeErrorwith context
DotEnvSource
- parses dotenv syntax (
export, quotes, inline comments) - requires non-empty
env_prefix(prefix scoping is mandatory) - nested keys are mapped with
nested_separator(default_) - with a
model, values are coerced to field types - recognized keys that fail coercion are captured in source
__unmapped_kwargs__
Example precedence inside one source:
APP_LOG={"level":"DEBUG"}
APP_LOG_LEVEL=WARN
APP_LOG_LEVEL overrides APP_LOG.level, regardless of line order.
EnvironSource
Same mapping/coercion behavior as DotEnvSource, but reads from os.environ.
env_prefix is mandatory, and failed coercions/unmatched keys are captured in
source __unmapped_kwargs__.
EnvironSource(env_prefix="APP", nested_separator="__")
# APP_LOG__LEVEL=ERROR -> {"log": {"level": "ERROR"}}
CliSource
Generates options from model fields (including nested fields).
Key behavior:
autogenerate=True(default): fields are exposed automaticallyautogenerate=False: only fields explicitly opted in via metadata are exposed- nested fields become flags like
--log-level - bools support both positive and negative forms:
--debug/--no-debug - nested struct fields also accept JSON on the top-level flag:
--log '{"level":"DEBUG"}'
- explicit nested flags override keys from that JSON
entry(..., cli=False)excludes a field from CLI generationentry(..., cli=True)force-includes a field whenautogenerate=Falseentry(..., cli_flag=..., cli_short_flag=...)overrides generated option names for that field- unknown CLI args are stored on source runtime state in
__unmapped_kwargs__, accessible also through method get_unmapped_payload() - unmatched CLI tokens (unknown flags and positionals) are stored on source runtime state in
__raw_argv__, retrievable by method get_raw_argv() - set
kebab_case=Falseto use dotted long flags (e.g.--log.level) - CLI accepts canonical and encoded/alias field names, and maps parsed values to encoded field names
Field policy precedence:
cli=False: field is excludedcli=True: field is includedcli_flag/cli_short_flagpresent: field is included- otherwise inclusion follows
autogenerate
src = CliSource(autogenerate=False, cli_args=["--server-host", "api"])
data = src.resolve(model=AppConfig)
print(data)
@datasources(CliSource())
class CliApp(DataModel):
dev: bool = False
# argv: ["prog", "--dev", "command", "test"]
cfg = CliApp()
print(cfg.dev) # True
print(cfg.get_raw_argv()) # ["command", "test"]
APISource
- performs an HTTP
GETrequest againstapi_url - optional auth header via
header_name+header_value - optional
root_nodeto unwrap wrapped payloads (for example{"data": {...}}) - request or parse failures raise
RuntimeErrorwith endpoint context
src = APISource(
api_url="https://example.com/config",
header_name="Authorization",
header_value="Bearer <token>",
root_node="data",
)
data = src.resolve()
Custom Source Example
When built-ins are not enough, implement DataSource.load(...).
from typing import Any
from msgspec_config import DataModel, DataSource, datasources
class SecretsSource(DataSource):
def load(self, model: type[DataModel] | None = None) -> dict[str, Any]:
# Replace this with Vault/AWS/GCP/etc.
return {"host": "secrets-host", "port": 8443}
@datasources(SecretsSource())
class ServiceConfig(DataModel):
host: str = "localhost"
port: int = 8080
Rationale: sources are deep-cloned per model instantiation, so source-local mutable state does not leak across DataModel() calls. DataSource.resolve(...) is the public finalized loader (reset + finalize); custom sources should override load(...).
Limitations
- Do not shadow
DataModel/DataSourcemethod names with fields; this is user responsibility and can break runtime behavior.
DataModel Helpers
DataModel is a msgspec.Struct configured as keyword-only and with dict-like output support.
Useful methods:
from_data(data)to create an instance from a Python mappingfrom_json(json_str)to create an instance from JSON bytes/stringmodel_dump()to get the model converted in Python builtinsmodel_dump_json(indent=...)for JSON outputmodel_json_schema(indent=...)for JSON Schema exportget_datasources_payload(*sources, **kwargs)to retrieve merged source payloads manuallyget_unmapped_payload()to lazily merge source runtime__unmapped_kwargs__in source order plus unknown constructor kwargs (merged last)get_raw_argv()to read raw CLI leftovers (unknown flags/positionals after mapped CLI options are filtered out)
Notes:
from_data(...)andfrom_json(...)ignore unknown keys.- Unknown keyword arguments passed to
DataModel(...)are available throughget_unmapped_payload().
Example:
cfg = AppConfig.from_json('{"host":"example.com","port":8081}')
print(cfg.model_dump())
print(AppConfig.model_json_schema(indent=2))
API Summary
DataModel: typed model base class with validation/serialization helpersDataSource: source base class (load(model=...) -> raw mapping,resolve(model=...) -> finalized mapping)datasources(*sources): decorator that attaches ordered source templatesentry(...): field helper with validation metadata and safe mutable defaultsgroup(...): helper for grouped object/list/dict fields- built-ins:
TomlSource,YamlSource,JSONSource,DotEnvSource,EnvironSource,CliSource,APISource
Development (Makefile + Commands)
The repository includes a Makefile to standardize common local tasks. Run targets from the project root.
Prerequisites:
uv- GNU Make (
make) - on Windows, use a GNU Make provider (for example Git Bash
makeormingw32-make)
Typical workflow:
make venv # install/update dependencies from lockfile
make ruff # format + lint autofix
make test # run tests
make docs # regenerate docs in ./docs
Run the full local pipeline:
make all
all expands to:
venv -> ruff -> test -> docs
Makefile targets:
make venv:uv syncmake docs:uv run pdoc -o ./docs --docformat google --favicon assets/msgspec-config-logo.svg --logo assets/msgspec-config-logo.svg --search -t ./docs --show-source msgspec_configmake ruff:uv run ruff format .anduv run ruff check --fix .make test:uv run pytestmake build:uv build --clear --no-sourcesmake publish-testpypi: runsmake build, thenuv publish --index testpypimake publish-pypi: runsmake build, thenuv publish
Equivalent direct commands (without make):
uv sync
uv run ruff format .
uv run ruff check --fix .
uv run pytest
uv run pdoc -o ./docs --docformat google --favicon assets/msgspec-config-logo.svg --logo assets/msgspec-config-logo.svg --search -t ./docs --show-source msgspec_config
uv build --clear --no-sources
Release (uv)
Build clean artifacts:
make build
Publish to TestPyPI first:
$env:UV_PUBLISH_TOKEN="pypi-<testpypi-token>"
make publish-testpypi
Publish to PyPI:
$env:UV_PUBLISH_TOKEN="pypi-<pypi-token>"
make publish-pypi
Packaging policy:
- wheel: runtime package only (
msgspec_config) - sdist: includes source, tests, and docs metadata for downstream builds/tests
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file msgspec_config-0.1.3.tar.gz.
File metadata
- Download URL: msgspec_config-0.1.3.tar.gz
- Upload date:
- Size: 126.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2030bd5eee62697a3c3c6c98678c333b8ed4dae7960676c2386adf7b3eab040
|
|
| MD5 |
a4774c3a6ee118515edfb03edf58509c
|
|
| BLAKE2b-256 |
ce6b81d5784cfb694e152d697a75e519f19842f8d1775eeaf4057f2db46bbb61
|
File details
Details for the file msgspec_config-0.1.3-py3-none-any.whl.
File metadata
- Download URL: msgspec_config-0.1.3-py3-none-any.whl
- Upload date:
- Size: 37.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.24 {"installer":{"name":"uv","version":"0.9.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc3666f9b6ee498b8077a139fd3b53c725da8b03febf422d3539c7ea504658a5
|
|
| MD5 |
720a5234ca16aec1c8e7284a27d7f228
|
|
| BLAKE2b-256 |
40f89065d85c5bbc7fc5160c4e5dcdef68f9d435b34e1fa55405429921f4ae93
|