anycfg
anycfg loads, layers and writes configuration in any registered
format with the standard Python serialization API — load, loads, dump,
dumps, as json and tomllib spell it — plus a Loader that merges many
sources in order, YAML !include, typed hydration, and a trust policy that is
untrusted by default. It was published as yaconfiglib up to 0.13.1; see
Migrating from yaconfiglib.
It requires pathapi, the path layer:
glob expansion with a portable **, directory-loop bounding, and path objects
for in-memory and remote files — and, below Python 3.11, tomli, so TOML
always reads. Everything else format- or feature-specific (PyYAML, tomli-w,
tplx and Jinja2, Pydantic) is optional and imported only when it is used.
Why anycfg
- One vocabulary.
load(path)anddump(obj, path)pick the format from the file name, sodump(x, "a.json")thenload("a.json")round-trips;loads(text, format=...)anddumps(obj, format=...)take content. A name no format claims is an error, never a guess. - Formats as plugins. JSON, YAML, TOML, INI and dotenv are built in. A
Formatsubclass registers in code or through an entry point, and every format passes the same shipped contract suite. - Layering.
Loader(merge="deep").load("base.yaml", "conf.d/*.yaml", "local.toml")merges in order, with deterministic glob ordering, three merge strategies or your own,DELETEto remove a key, andcollect=to gather documents instead of merging them. - Sources beyond files. Any pathapi
Path, open files, Python mappings, the environment, in-memory text and command output — see Sources. - Includes.
!includein YAML, relative to the including file at every depth, with cycle detection and a load budget. - Typed results, and the type steers the parse.
load(..., into=Model)builds a dataclass, aTypedDict, a Pydantic model or any class, coercing every field without losing data. YAML'sNO,1.10and0755reach astrfield exactly as written, while aboolorintfield gets the value YAML read.dump()writes typed results back, so loading into a type and dumping round-trips — see Typed results. - Templates.
Loader(interpolate=True)renders{{ ... }}in values, andapp.yaml.j2— or any engine tplx knows,app.toml.liquidincluded — renders a whole file first, sandboxed by default — see Templating. - Safe by default. Commands and template files are off, interpolation is sandboxed, reads can be confined to directories, and errors never carry document text — see Security.
Installation
pip install anycfg
That brings pathapi with it. Add the extras for the features you use:
pip install "anycfg[yaml,toml,templates]"
| Extra | Adds | Needed for |
|---|---|---|
yaml |
PyYAML 6 | the YAML format and !include |
toml |
tomli-w |
writing TOML (reading needs no extra: tomli comes with anycfg below Python 3.11) |
templates |
tplx[jinja] 0.2+ |
interpolation and template files, with Jinja |
all |
every extra above |
A template engine other than Jinja needs its own tplx extra
(pip install "tplx[liquid]"). JSON, INI and dotenv need nothing beyond the
standard library. import
anycfg imports none of the extras; a feature whose extra is missing
raises MissingDependencyError naming the extra to install.
Quick start
One document:
import anycfg
config = anycfg.load("settings.yaml") # format from the extension
data = anycfg.loads('{"a": 1}', format="json")
anycfg.dump(config, "settings.json") # written as JSON
A few layers, later over earlier, None for one that is absent:
config = anycfg.load("base.toml", "local.toml", merge="deep")
config = anycfg.load(*anycfg.default_configs("app.toml"), default={})
Layers, with dot access:
from anycfg import Loader
loader = Loader(base_dir="conf", merge="deep", wrap=True)
config = loader.load("base.yaml", "conf.d/*.yaml", "local.toml", None)
print(config.database.host)
Later sources override earlier ones, a glob's matches load in a fixed order,
and None stands for an optional layer that is absent.
Typed:
import dataclasses
import anycfg
@dataclasses.dataclass
class Database:
host: str
port: int
db = anycfg.load("db.json", into=Database) # "5432" becomes 5432
anycfg.dump(db, "db.yaml") # typed data writes back
Includes and interpolation, in app.yaml:
base: /srv/app
logs: "{{ base }}/logs"
database: !include db.yaml
config = Loader(interpolate=True).load("app.yaml")
The environment and commands, which you opt into:
from anycfg import Loader, Policy
from anycfg.sources import Env
config = Loader(policy=Policy.trusted()).load(
"base.yaml",
"cmd+json://vault kv get -format=json secret/app",
Env("APP_", nested="__"), # APP_DB__PORT -> {"DB": {"PORT": ...}}
)
Where to go next
- Guide — task-oriented pages: formats, sources, merging, includes, templating, typed results and security.
- Migrating — every name and keyword of the previous API, mapped to its replacement.
- API Reference — generated from the docstrings, organized by module.
- Changelog — release history.
The installed package also carries its API header, anycfg/AGENTS.md:
every public name with its signature, defaults and gotchas, in one file.