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 and REPL Support: Full type-hinting and IntelliSense/code-completion 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

IDE & REPL auto-completion demo

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')                      # ok!
    print(f"Running {conf.app_name} on port {conf.port}")           # 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

  • ⚙️ codeberg ci-pipeline: pytest on every push, publish to PyPi on new "v*" tag
  • ✔️ YAML support
  • not-ok: ✖️ unknown: ❔

Changelog

  • v0.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

Demo-A

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']

Demo-B

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

import logging
#logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')

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_b.toml', cli_help_enabled=True)
    print(f"Running app {conf.app_name} on port {conf.port}, DB {conf.db.user}@{conf.db.con} pwd={''.join('*' for _ in conf.db.pwd)}")
except UserNeedsHelp as hlp:
    print(hlp)
except ConfigError as e:
    print(e)
# config_b.toml
app_name="TOML-app3"
port=7777

[db]
con="https://example.com/db"
user="db_readonly"
bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_b.py
<typedconf> Loading field 'pwd' failed! 1 validation error for AppConfig db.pwd Field required [type=missing, input_value={'con': 'https://example...., 'user': 'db_readonly'}, input_type=dict] For further information visit https://errors.pydantic.dev/2.11/v/missing
1 validation error for AppConfig db.pwd Field required [type=missing, input_value={'con': 'https://example...., 'user': 'db_readonly'}, input_type=dict] For further information visit https://errors.pydantic.dev/2.11/v/missing

bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_b.py --help
--cfg_app_name (AppConfig.app_name)
  type=str, default=None
  Application name

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

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

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

--cfg_port (AppConfig.port)
  type=int, default=8080
  Port

bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_b.py --cfg_db__pwd=asdfsd
Running app TOML-app3 on port 7777, DB db_readonly@https://example.com/db pwd=******

Animated GIF

Demo-C

"""TypedConf Demo-C"""
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
        file='config_c.yaml',      # load data from a single file (toml,json,yaml)
        toml=['config_c.toml'],    # load data from TOML file-list
        yaml=['config_c.yaml'],
        json=['config_c.json'],
        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(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. 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)

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

print(f"Try `uv run {script_fn} --help`")

config_c.toml

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

config_c.json

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

config_c.yaml

app: "YAML-app"
tags:
  - "XXX"
  - "YYY"
  - "ZZZ"
  - "AAA"
bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_c.py
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=True, pyyaml=True]
DEBUG - <typedconf> json read from config_c.json
DEBUG - <typedconf> yaml data read from: config_c.yaml
DEBUG - <typedconf> toml data read from: config_c.toml
DEBUG - <typedconf> yaml data read from: config_c.yaml
DEBUG - <typedconf> cli nothing found. prefix: '--cfg_' seperator: '__'
DEBUG - <typedconf> env nothing found. prefix: 'CFG_' seperator: '__'
INFO - <typedconf> ConfigModel 'MyConfig' loaded (readonly)
== JSON ==========================================
{
  "app": "YAML-app",
  "log_enabled": true,
  "loglevel": 0,
  "url": "http://myserver.com:1234/mydatabase",
  "tags": [
    "XXX",
    "YYY",
    "ZZZ",
    "AAA"
  ]
}
==================================================
Try `uv run main_demo_c.py --help`
bernd@Venus:~/projects/typedconf/demo$

Demo-D

import logging
from enum import Enum
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-D. 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
""")
    exit(1)

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

print(f"Try `uv run {script_fn} --help`")
bernd@Venus:~/projects/typedconf/demo$ uv run main_demo_d.py
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=True, pyyaml=True]
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_demo_d.py --help`
bernd@Venus:~/projects/typedconf/demo$

Release files for typedconf 0.9.5

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.5
File Size Uploaded
typedconf-0.9.5.tar.gz 862.4 kB Details

Built distribution (wheel)

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

Total release size: 879.4 kB

Release files / typedconf-0.9.5.tar.gz

Download URL typedconf-0.9.5.tar.gz
Size 862.4 kB
Tags Source
SHA-256 checksum
How to use checksums
162643edd0eef2dcb70fb7b8c3d4fabf1736d31672b025861cd8011a9bbfff56
BLAKE2b-256 checksum
How to use checksums
418fbe5b9d86347df1b052bd32e78ad99decfafbfa081e289529b582c63b87e0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

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

Download URL typedconf-0.9.5-py3-none-any.whl
Size 16.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
199a7093609cd197a8167eb721757bb30cf4fa626b0074b28f3e348430db7b89
BLAKE2b-256 checksum
How to use checksums
11becf355f13df4fc08448885d38463f03fa5f318064d92b5a0b0846e3691c29
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

0.9.5 This release

2 release files

0.9.4

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