Skip to content

conf

Define configuration as ordinary Python classes, bind fields to environment variables, and override values for the current contextvars context.

uv add liblaf-conf

liblaf-conf supports Python 3.12 and newer.

Define Settings

Subclass BaseConfig, declare values with Field or a field_* helper, and compose nested sections with group().

from liblaf import conf


class DatabaseConfig(conf.BaseConfig):
    url: conf.Field[str] = conf.field_str(default="sqlite:///app.db")


class AppConfig(conf.BaseConfig):
    debug: conf.Field[bool] = conf.field_bool(default=False)
    port: conf.Field[int] = conf.field_int(env="PORT", default=8000)
    hosts: conf.Field[list[str]] = conf.field_list_str(default=["localhost"])
    database: conf.Group[DatabaseConfig] = conf.group(DatabaseConfig)

AppConfig() is a cached singleton. Accessing cfg.port returns a Var, and that variable stores its active value in a ContextVar.

Load, Set, and Override

Call load_env() when string values should come from the environment. Call set() when you already have Python values. For nested groups, pass nested mappings.

cfg = AppConfig()

cfg.load_env()
cfg.set(database={"url": "sqlite:///dev.db"})

with cfg.override(debug=True, database={"url": "sqlite:///test.db"}):
    assert cfg.debug.get() is True
    assert cfg.database.url.get() == "sqlite:///test.db"

assert cfg.database.url.get() == "sqlite:///dev.db"

Use the field Var directly when only one value needs to change.

token = cfg.port.set(9000)
try:
    assert cfg.port.get() == 9000
finally:
    cfg.port.reset(token)

Convert Environment Strings

Environment variables are strings, so each field can carry a converter. The helper constructors cover common cases:

  • field_bool, field_int, field_float, field_decimal, and field_str for scalar values.
  • field_json, field_list_str, and field_path for structured values.
  • field_date, field_datetime, field_time, and field_timedelta for temporal values backed by Pydantic validation.
class WorkerConfig(conf.BaseConfig):
    retries: conf.Field[int] = conf.field_int(default=3)
    labels: conf.Field[list[str]] = conf.field_list_str(delimiter=";")

For lower-level control, pass your own converter to field(), Field, or Var.

Serialize the Active State

Use to_dict() when another library needs ordinary dictionaries. Use to_namespace() when attribute access is more convenient.

cfg.set(port=8000, debug=True, hosts=["localhost", "example.test"])

assert cfg.to_dict() == {
    "debug": True,
    "port": 8000,
    "hosts": ["localhost", "example.test"],
    "database": {"url": "sqlite:///dev.db"},
}

to_dict() reads the same active context as get(), so temporary overrides are reflected while the override is active and disappear when it exits.

API Map