Skip to main content

TypedConf

A lightweight, type-safe configuration management library powered by Pydantic. Following the 12-factor application guide, it centralizes your application configuration/settings by merging data from multiple sources with a defined priority: env > cli > files > payload > defaults.

TypedConf combines the strengths of Pydantic and Dynaconf: While it requires developers to define classes to reflect the configuration structure (unlike Dynaconf), this approach heavily reduces headaches 🤕🤕😠 caused by mistyped property names, wrong types, or formatting errors. TypedConf offers comprehensive IDE support, type safety, and runtime data validation, powered by Pydantic.

Thanks to Pydantic, after parsing and validation, TypedConf guarantees that the fields of the resultant model instance will conform to the field types defined on the subclass of ConfigModel. TypedConf offers comprehensive IDE support, type safety, and runtime data validation.

Key Features

  • Type-Safe: Built on Pydantic, ensuring configuration values are validated at runtime.
  • IDE Support: Full type-hinting and IntelliSense support for seamless development.
  • Nested Support: Easily handle complex configuration structures.
  • TOML, YAML and JSON Interface: Load configuration from various file formats.
  • CLI and ENVironment Interface: Load configuration data from CLI arguments (--cfg_myint=1) and ENV variables (export CFG_MYINT=1).
  • Layered Configuration: Merges configuration data with a clear priority: env > cli > files > payload > defaults (env has highest priority).
  • Immutability: Configuration data is read-only (frozen) by default after loading.
  • Self-Documenting: Generate help text directly from your configuration schema definition.

Quick Start

Instead of defining scattered constants at the beginning of a script, use ConfigModel to manage your application configuration centrally, type-safely, and with built-in validation.

from typedconf import ConfigModel

# Define configuration schema
class AppConfig(ConfigModel):
    app_name: str       # Required field, no default
    port: int = 8080    # Default value

# Load configuration from payload
conf = AppConfig.load(payload={'app_name': 'app'})
print(f"Running {conf.app_name} on port {conf.port}")   # Running app on port 8080

Load Configuration from TOML, YAML or JSON

Most of the time you will load data from a configuration file. Let's say from this TOML file (config.toml):

# config.toml
app_name = "myapp"
port = 2000
from typedconf import ConfigModel

class AppConfig(ConfigModel):
    app_name: str
    port: int = 8080

# Load configuration from defaults and toml-file
conf = AppConfig.load(file='config.toml')
print(f"Running {conf.app_name} on port {conf.port}")   # Running myapp on port 2000

Some uses cases need more than just a single configuration file. So there are few ways to load data from files:

  • file - A single configuration file to load (toml, json, yaml - based on the file extension)
  • toml - List of TOML files
  • yaml - List of YAML files
  • json - List of JSON files

The files will be loaded and deep-merged in this order: file > toml > yaml > json (file has highest priority). Lists will be loaded from left (idx=0) to right.

Default behaviour for loadiing data from files is "Raise a ConfigError on any file error (i.e. file not found, parser error)". Set skip_errors=True to override this behaviour, like saying "YES, ignore all file and parsing errors and continue loading and validating data".

Note: loading yaml files requires optional python package pyyaml. loading toml-files on python < 3.11 requires optional python package tomli.

Data Validation and Documentation

ConfigModel is a Pydantic BaseModel. You can use Field to add descriptions, constraints, or validation rules.

Loading a configuration means:

  1. load and parse data from all sources.
  2. deep-merge all data together, where data from higher priority source overwrites data from lower priority sources.
  3. create and validate a new ConfigModel instance with the merged data.

Loading the configuration may raise a ConfigError (on step 1 or 3). ConfigError is just a wrapper around pydantic's ValidationError.

from pydantic import Field
from typedconf import ConfigModel, ConfigError

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="Application name. required field.")
    port: int = Field(8080, ge=1000, le=9999, description="Port to listen on (1000-9999). optional field. default 8080")

try:
    conf1 = AppConfig.load(file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}")  # ok, prints: Running myapp on port 2000

    conf2 = AppConfig.load(payload={'app_name': 'app', 'port': 20}) # fail! port is invalid
except ConfigError as e:
    print("Validation failed! Can't load configuration:", e)

Nested Configuration

Nested configurations can be applied by nesting ConfigModel classes.

from pydantic import Field
from typedconf import ConfigModel, ConfigError

class DatabaseConfig(ConfigModel):
    con: str = Field(..., description="DB connection string")
    user: str = Field(..., description="DB username")
    pwd: str = Field(..., description="DB password")

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="Application name")
    port: int = Field(8080, ge=1000, le=9999, description="Port")
    db: DatabaseConfig = Field(default_factory=DatabaseConfig, description="Database configuration")

try:
    conf = AppConfig.load(file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}. DB user: {conf.db.user}")
except ConfigError as e:
    print(e)

If your TOML file doesn't match the new nested structure (e.g., missing [db] section), TypedConf/Pydantic will raise a ConfigError:

3 validation errors for DatabaseConfig 
con Field required [type=missing, ...]
user Field required [type=missing, ...]
pwd Field required [type=missing, ...]

Let's fix our TOML file. Remember: It's best practice not to store sensitive data like passwords inside configuration files!

# config.toml
app_name = "toml-app"
port = 9090

[db]
con = "postgresql://localhost:5432/mydb"
user = "db_user_readonly"

ENV & CLI Interface

You can inject missing or secret data (like database passwords) through the CLI or environment variables. Both interfaces are enabled by default.

# CLI interface
$ python app.py --cfg_db__pwd="secret"
Running toml-app on port 9090.

# Environment variables interface
$ export CFG_DB__PWD="secret"
$ python app.py

# Combining them
$ export CFG_DB__USER="db_user_admin"
$ export CFG_DB__PWD="secret"
$ CFG_PORT=2525 python app.py --cfg_app_name="cli-app"

Conventions:

  • Case sensitivity: CLI is lowercase, ENV is UPPERCASE.
  • CLI uses long format arguments like --key=val.
  • Prefix: Both use cfg_ by default (customizable via cli_prefix).
  • Separator: Nested fields are separated by __ (customizable via cli_separator).

Priority Chain

TypedConf merges all data sources in a specific order. Higher-priority sources overwrite lower-priority ones:

  1. Environment Variables (Highest): e.g., export CFG_DB__PWD="abc"
  2. CLI Arguments: e.g., --cfg_db__pwd='abc'
  3. Single File
  4. TOML Files: Merged from left to right
  5. YAML Files: Merged from left to right
  6. JSON Files: Merged from left to right
  7. Payload: Dictionary passed directly via .load(payload=...)
  8. Defaults (Lowest): Defined inside the ConfigModel class

Note: The system performs a deep merge, preserving nested structures when partial overrides are provided.

CLI Help included

TypedConf can include a --help argument to your application and generates a nice helptext for all field-names. Let's step back to our nested configuration example and add help for the user by using user_needs_help()

from pydantic import Field
from typedconf import ConfigModel, ConfigError

RFC3986_URI_REGEX = r'^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})(?::\d+)?([\/\w \.-]*)*\/?$'

# define configuration schema
class DatabaseConfig(ConfigModel):
    con: str = Field(..., pattern=RFC3986_URI_REGEX, description="DB connection-string, required field.")
    user: str = Field(..., description="DB username, required field.")
    pwd: str = Field(..., description="DB password, required field.")

class AppConfig(ConfigModel):
    app_name: str = Field(..., description="application name, required field.")
    port: int = Field(8080, gt=1000, lt=9999, description="application listen on port. Between 1000 and 9999, defaullt=8080")
    db: DatabaseConfig = Field(default_factory=DatabaseConfig, description="database configuration")

# need some help?
if AppConfig.user_needs_help():
    print(f"MYAPP Help\n\nAvailable CLI Parameter\n{AppConfig.get_cli_helptext()}")
    exit(0)

# Load configuration
try:
    conf = AppConfig.load(file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}. DB connected {conf.db.user} @ {conf.db.con}")
except ConfigError as e:
    print(e)

Same effect creates cli_help_enabled=True while loading. This raises a UserNeedsHelp Exception, containing a propper version of the help-text, with respect to cli_prefix and cli_separator.

try:
    conf = AppConfig.load(cli_help_enabled=True, file='config.toml')
    print(f"Running {conf.app_name} on port {conf.port}")
except UserNeedsHelp as e:
    print(f"MYAPP Help\n\nAvailable CLI Parameter\n{e}")
    exit(0)
except ConfigError as e:
    print(e)

However, TypedConf adds in both versions an additional --help flag to the CLI interface:

$ python main.py --help
MYAPP Help

Available CLI Parameter
--cfg_app_name (AppConfig.app_name)
   type=str, default=None
   application name, required field.

--cfg_db__con (DatabaseConfig.con)
   type=str, default=None
   DB connection-string, required field.

--cfg_db__pwd (DatabaseConfig.pwd)
   type=str, default=None
   DB password, required field.

--cfg_db__user (DatabaseConfig.user)
   type=str, default=None
   DB username, required field.

--cfg_port (AppConfig.port)
   type=int, default=8080
   application listen on port. Between 1000 and 9999, defaullt=8080

Mutable Configuration

ConfigModel sets the pydantic model_config to:

model_config = {
    "frozen": True,
    "extra": "forbid",
    "validate_default": True,
    "validate_assignment": True,
}
  • Set ConfigModel to readonly
  • Raise error, when loading unknown extra data
  • Validate default values when loading
  • validate when assigning a new value to a writeable ConfigModel

If you explicitly need a mutable/writable configuration at runtime, set (all nested) Pydantic's frozen Model_config to False:

class AppConfig(ConfigModel):
    model_config = {'frozen': False}  # Makes pydantic basemodel mutable
    
    app_name: str
    port: int = 8080

conf = AppConfig.load(payload={'app_name': 'test'})
conf.port = 9999  # is now valid

Exporting Configurations

Export your current configuration instance to TOML, YAML or JSON format.

print(conf.dumps_toml())
print(conf.dumps_yaml())
print(conf.dumps_json())

Note: Exporting to TOML requires python package tomli-w, loading and exporting yaml requires package pyyaml

Install

pip install typedconf                          # minimal — nur pydantic
pip install "typedconf[dotenv]"                # .env support
pip install "typedconf[toml_export]"           # TOML export (dumps_toml)
pip install "typedconf[toml_read]"             # TOML import (python <3.11)
pip install "typedconf[yaml]"                  # YAML support (load and dumps)
pip install "typedconf[all]"                   # TOML write, YAML, dotenv
pip install "typedconf[dev]"                   # development: all + pytest
pip install "typedconf[dotenv,toml_export]"

TODOs

  • ✅ YAML support
  • ✅ codeberg ci-pipeline: pytest on every push, publish to PyPi on new "v*" tag

Changelog

-0.9.4

  • YAML Support
  • load a single-File (toml or json or yaml))
  • v0.9.3
    • published to PyPi
  • v0.9.0
    • cleanup code for 1st release/tag

License

This project is licensed under the GPL-3.0 License - see the LICENSE file for details.

Demos

TypedConf Demo 00

from typedconf import ConfigModel

class MyConfig(ConfigModel):
    app: str = 'myapp'
    log_enabled: bool = False
    tags: list[str] = ['a', 'b']

conf = MyConfig.load()
print(conf)     # app='myapp' log_enabled=False tags=['a', 'b']

TypedConf Demo-01

import sys
import logging
from typedconf import ConfigModel, ConfigError, UserNeedsHelp
from pydantic import Field

logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')
RFC3986_URI_REGEX = r'^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})(?::\d+)?([\/\w \.-]*)*\/?$'


## CONFIGURATION
class MyConfig(ConfigModel):
    """Configuration for this script. In it's core ConfigModel is a pydantic Basemodel"""
    app: str
    log_enabled: bool = False
    loglevel: int = Field(0, ge=0, le=2, description="database log-level: 0=no logging, 1=errors, 2=info. Default 0")
    url: str = Field("http://myserver.com:1234/mydatabase", pattern=RFC3986_URI_REGEX, description="load data from this URL. validates url against RFC3986")
    tags: list[str] = Field(['a', 'b'], description="A list of strings. Hint: json list-style input is available in CLI, but not in ENV")


## MAIN
script_fn = sys.argv[0]
try:
    conf = MyConfig.load(
        payload={'app':'payload-app'},  # source payload
        toml=['config-demo01.toml'],    # load data from TOML file(s)
        cli_help_enabled=True,          # enable --help in CLI, raises UserNeedsHelp
        skip_errors = True              # ignore errors while loading and parsing files. Default = False. NB: skip_errors does not effect pydantic's validation
    )
    print(conf)
    print(f"== JSON {'=' * 42}\n{conf.dumps_json()}\n{'=' * 50}") # conf.dumps_toml() available, if python package 'tomli_w' is installed

except UserNeedsHelp as e:
    help_config_fields = e
    help_message = f"""\
TypedConfig Demo-01.
Loads config from default, payload, toml-file (ignoring file errors). Available CLI arguments / ENV variables
{help_config_fields}

Examples:
uv run {script_fn}  # loads config from defaults and payload (and files)
uv run {script_fn} --cfg_app=cli-app  # overwrite config from CLI
uv run {script_fn} --cfg_app=cli-app --cfg_loglevel=2 --cfg_log_enabled=1 --cfg_tags='["x","y"]'
CFG_APP='env-app' uv run {script_fn} --cfg_app=cli-app  # ENV overrides CLI
uv run {script_fn} --cfg_loglevel=9  # validation failed
"""
    print(help_message)

    sys.exit(1)
except ConfigError as e:
    print(f"⚠️ ConfigError raised! {e}")
    sys.exit(2)

print(f"Try `uv run {script_fn} --help`")
sys.exit(0)

config-demo01.toml

app="toml-app"
tags=["XXX", "YYY", "ZZZ"]

config-demo01.json

{
  "app": "JSON-app",
  "log_enabled": true
}

config-demo01.yaml

app: "yaml-app"
tags:
  - "XXX"
  - "YYY"
  - "ZZZ"
  - "AAA"
DEBUG - <typedconf> LibWrapper using python 3.12.3, package to read toml [tomllib=True, tomli=False], optional package to write toml [tomli_w=True], optional package [dotenv=False]
WARNING - <typedconf> failed to read and parse toml file 'XX_config_demo01.toml'! file not found.
DEBUG - <typedconf> cli nothing found. prefix: '--cfg_' seperator: '__'
DEBUG - <typedconf> env nothing found. prefix: 'CFG_' seperator: '__'
INFO - <typedconf> ConfigModel 'MyConfig' loaded (readonly)
app='payload-app' log_enabled=False loglevel=0 url='http://myserver.com:1234/mydatabase' tags=['a', 'b']
== JSON ==========================================
{
  "app": "payload-app",
  "log_enabled": false,
  "loglevel": 0,
  "url": "http://myserver.com:1234/mydatabase",
  "tags": [
    "a",
    "b"
  ]
}
==================================================
Try `uv run main_demo01.py --help`

TypedConf Demo-02

import logging
from enum import Enum
import sys
from pydantic import Field
from typedconf import ConfigError, ConfigModel, UserNeedsHelp

logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')
script_fn = sys.argv[0]


class StatusEnum(str,Enum):
    none  = "NONE"
    ok    = "OK"
    nok   = "NOT-OK"
    warn  = "WARNING"
    err   = "ERROR"

## configuration-schema
class L2(ConfigModel):
    name:str = 'L2'
    d2: int = Field(2, description="data L2-D2")
    d2b: list[str] = Field(['a','b','c'], description="list-data L2-D2b")
    d2c: int = Field(1111, ge=1000, le=9999, description="int-data L2-D2c, validate between 1000 and 9999.")
    d2d: StatusEnum = Field(StatusEnum.none, description="Last known status. Default none")

class L1(ConfigModel):
    name:str = 'L1'
    d1: int = Field(1, description="data L1-D1")
    l2: L2 = Field(default_factory=L2, description="link to a L2")
    
class L0(ConfigModel):
    name:str = 'L0'
    d0: int = Field(0, description="data L0-D0")
    l1: L1 = Field(default_factory=L1, description="link to a L1")

## main
try:
    conf = L0.load(
        payload={'name':'payload_L0', 'l1':{'name':'payload_L1', 'l2':{'d2c':2222, 'd2d':StatusEnum.ok}}},
        cli_help_enabled=True,          # --help enabled
        cli_prefix='',                  # no prefix for cli, env is disabled
        env=False,
        cli=True,
    )
    if conf.dumps_toml():
        print(f"== TOML {'=' * 42}\n{conf.dumps_toml()}\n{'=' * 50}")   # optional tomli_w is installed :)
    else:
        print(f"== JSON {'=' * 42}\n{conf.dumps_json()}\n{'=' * 50}")   # json fallback

except UserNeedsHelp as e:
    cli_args_helptxt = e
    print(f"""\
TypedConfig Demo-02. Available cli/env arguments:

{cli_args_helptxt}

Examples:
# 1) loads config from defaults and payload
  uv run {script_fn}
# 2) cli interface (no prefix, env is disabled)
  uv run {script_fn}  --name='cli_L0'
  uv run {script_fn} --l1__l2__d2d=ERROR         # using enum-values
  uv run {script_fn} --l1__l2__d2b='["x","y"]'   # using json-style array
# 3) data validation
  uv run {script_fn} --l1__l2__d2c=7777          # ok
  uv run {script_fn} --l1__l2__d2c=77            # fail
  uv run {script_fn} --l1__l2__d2d='WARNING      # ok
  uv run {script_fn} --l1__l2__d2d='UNKNOWN'     # fail
""")
    sys.exit(1)

except ConfigError as e:
    print(f"Configuration ERROR: {e}")
    sys.exit(2)

print(f"Try `uv run {script_fn} --help`")
sys.exit(0)
DEBUG - <typedconf> LibWrapper using python 3.12.3, package to read toml [tomllib=True, tomli=False], optional package to write toml [tomli_w=True], optional package [dotenv=False]
DEBUG - <typedconf> cli nothing found. prefix: '--' seperator: '__'
INFO - <typedconf> ConfigModel 'L0' loaded (readonly)
== TOML ==========================================
name = "payload_L0"
d0 = 0

[l1]
name = "payload_L1"
d1 = 1

[l1.l2]
name = "L2"
d2 = 2
d2b = [
    "a",
    "b",
    "c",
]
d2c = 2222
d2d = "OK"

==================================================
Try `uv run main_demo02.py --help`

Release files for typedconf 0.9.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for typedconf 0.9.4
File Size Uploaded
typedconf-0.9.4.tar.gz 45.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for typedconf 0.9.4
File Interpreter ABI Platform
typedconf-0.9.4-py3-none-any.whl Python 3 none any Details

Total release size: 62.2 kB

Release files / typedconf-0.9.4.tar.gz

Download URL typedconf-0.9.4.tar.gz
Size 45.8 kB
Tags Source
SHA-256 checksum
How to use checksums
cae1393ed85f1a89d29e3e8e1ab04af98f6044ab9c6300481baa8cb995b0cefa
BLAKE2b-256 checksum
How to use checksums
26b74588e01862b78676f4056b5b52fac0dfe4cb5d027939d142afe32c3e3b6b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / typedconf-0.9.4-py3-none-any.whl

Download URL typedconf-0.9.4-py3-none-any.whl
Size 16.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0a41d0ea65b4782d0aaeca3be6b70cb1addde60a8e66f34ecdb0426fe81561bd
BLAKE2b-256 checksum
How to use checksums
c1b55a65d7a51235703ba7bbe36d099d5d8662e195e8e29e6430ec86b886ad2d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

0.9.5

2 release files

This release

0.9.4 This release

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page