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
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 filesyaml- List of YAML filesjson- 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:
- load and parse data from all sources.
- deep-merge all data together, where data from higher priority source overwrites data from lower priority sources.
- 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 viacli_prefix). - Separator: Nested fields are separated by
__(customizable viacli_separator).
Priority Chain
TypedConf merges all data sources in a specific order. Higher-priority sources overwrite lower-priority ones:
- Environment Variables (Highest): e.g.,
export CFG_DB__PWD="abc" - CLI Arguments: e.g.,
--cfg_db__pwd='abc' - Single File
- TOML Files: Merged from left to right
- YAML Files: Merged from left to right
- JSON Files: Merged from left to right
- Payload: Dictionary passed directly via
.load(payload=...) - Defaults (Lowest): Defined inside the
ConfigModelclass
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)
| File | Size | Uploaded | |
|---|---|---|---|
| typedconf-0.9.5.tar.gz | 862.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|