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 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' - JSON Files: Merged from left to right
- TOML 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(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)
| File | Size | Uploaded | |
|---|---|---|---|
| typedconf-0.9.1.tar.gz | 37.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|