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 > json > toml > 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.

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 and JSON Interface: Load configuration from TOML and/or JSON files.
  • CLI and Environment Interface: Load configuration data from CLI arguments (--cfg_myint=1) and/or ENV variables (export CFG_MYINT=1).
  • Layered Configuration: Merges configuration data with a clear priority: env > cli > json > toml > payload > defaults.
  • 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 (here directly 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 (or JSON)

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

app_name = "myapp"
port = 2000
from typedconf import ConfigModel

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

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

Data Validation and Documentation

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

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).")

try:
    conf = AppConfig.load(toml=['config.toml'])
    print(f"Running {conf.app_name} on port {conf.port}")
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(toml=['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. JSON Files: Merged from left to right
  4. TOML Files: Merged from left to right
  5. Payload: Dictionary passed directly via .load(payload=...)
  6. 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(toml_files=['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, toml_files=['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 JSON or TOML format.

# Export TOML string (requires optional python package 'tomli_w')
print(conf.dumps_toml())

# Export JSON string
print(conf.dumps_json())

Note: Exporting to TOML requires python package tomli-w.

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 (nur Python <3.11)
pip install "typedconf[all]"                   # alles
pip install "typedconf[dev]"                   # Entwicklung: pytest + alles
pip install "typedconf[dotenv,toml_export]"    # kombiniert

TODOs

  • 🙈

Changelog

  • 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.toml'],   # load data from TOML file
        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)
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.1

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.1
File Size Uploaded
typedconf-0.9.1.tar.gz 37.1 kB Details

Built distribution (wheel)

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

Total release size: 52.0 kB

Release files / typedconf-0.9.1.tar.gz

Download URL typedconf-0.9.1.tar.gz
Size 37.1 kB
Tags Source
SHA-256 checksum
How to use checksums
7cad9ecd8e568fe9a692b222c99b42371b58f44c7fdea77322c209e1910397cc
BLAKE2b-256 checksum
How to use checksums
7af291549aa5f5d1827b45dd869c8b2a1b83b2caff36cc9726755f5df2fc526e
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.1-py3-none-any.whl

Download URL typedconf-0.9.1-py3-none-any.whl
Size 14.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
387d47a42d125e88b726b9ad405b5785ef5fc59e360b3e6b27f8d5bbf269287a
BLAKE2b-256 checksum
How to use checksums
be55a1baea86b6cb339d3f53196a9c9ee2ef26003dd949700a44df155f60c6a1
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

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

This release

0.9.1 This release

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