ConfigPlusPlus
CONFIG, MADE LEGIBLE
Typed configuration for Python — load it from environment variables or YAML, display it grouped and readable, and mask secrets automatically. A small, dependency-light library meant to be shared across every service in a stack.
Declare a config class, read each field with
env(...)or from a YAML file, and
Why ConfigPlusPlus
- One obvious place for configuration. A config is a class; every
UPPERCASEattribute is a field. No scatteredos.getenvcalls, no untyped dictionaries. - Readable by default.
print(MyConfig)renders a boxed, prefix-grouped, aligned view — on the class itself, no instance required. - Secrets never leak into logs. Fields whose name contains
SECRET,API_KEY,PASSWORD,TOKENorCREDENTIALare masked automatically, everywhere the config is displayed. - Typed, with a precise casting contract.
env(..., cast=int|bool|float|pathlib.Path)with a documented, stable boolean rule — the same across every service that depends on it.
Installation
pip install configplusplus
# or
poetry add configplusplus
Requires Python 3.10+.
Quickstart
from configplusplus import EnvConfigLoader, env
import pathlib
class AppConfig(EnvConfigLoader):
DATABASE_HOST = env("DATABASE_HOST") # required (raises if missing)
DATABASE_PORT = env("DATABASE_PORT", cast=int) # typed
DATA_DIR = env("DATA_DIR", cast=pathlib.Path) # pathlib.Path
DEBUG_MODE = env("DEBUG_MODE", cast=bool, default=False) # optional with default
SECRET_API_KEY = env("SECRET_API_KEY") # masked on display
print(AppConfig.DATABASE_HOST) # -> 'localhost'
print(AppConfig) # -> grouped, aligned, masked view
print(AppConfig) renders:
╔════════════════════════════════════════════╗
║ APPCONFIG ║
╚════════════════════════════════════════════╝
▶ API
API_ENDPOINT = 'https://api.example.com'
API_KEY = 'key…56 (hidden)'
▶ DATABASE
DATABASE_HOST = 'localhost'
DATABASE_PORT = 5432
▶ DEBUG
DEBUG_MODE = False
▶ SECRET
SECRET_JWT_KEY = 'sk_…89 (hidden)'
Only UPPERCASE, non-callable attributes are part of a config. Fields are grouped by the prefix
before the first underscore (DATABASE_HOST + DATABASE_PORT → DATABASE).
EnvConfigLoaderbodies are evaluated at import time — the environment must already be populated when the class is defined. Load your.envbefore importing the config (see below).
The env() casting contract
env(key: str, *, default=None, cast=str, required=True)
| Argument | Default | Behaviour |
|---|---|---|
cast |
str |
int, float, bool, pathlib.Path, or any 1-arg callable |
default |
None |
Returned when the variable is unset |
required |
True |
Raise RuntimeError if unset and no default is provided |
env_optional(key, *, default=None, cast=str) is the shorthand for required=False.
env_list(key, *, default=None, sep=",", cast=str, required=True) reads a delimited value as
a list ("a, b ,c" → ["a", "b", "c"]; cast=int on "80,443" → [80, 443]).
Boolean casting (cast=bool) — these strings are False; everything else is True:
"false" "False" "FALSE" "0" "no" "No" "NO" ""
Loading .env files
from configplusplus import safe_load_envs
safe_load_envs() # load ./.env (default)
safe_load_envs(".env") # explicit file
safe_load_envs("config/.env") # nested file
safe_load_envs("./config") # a directory: loads every *.env inside it
safe_load_envs(verbose=False) # silent
# Typical entrypoint: load the environment BEFORE importing config classes.
Returns True if at least one file was loaded, False otherwise. Accepts a str or a
pathlib.Path, a single *.env file or a directory of them.
YAML configuration
from configplusplus import YamlConfigLoader
class UiConfig(YamlConfigLoader):
def __post_init__(self) -> None:
self.app_name = self._raw_config["application"]["name"]
self.theme = self._raw_config["display"]["theme"]
config = UiConfig("config.yaml")
config.get("database.host") # dot-notation access
config.get("api.timeout", default=30) # with a fallback
config.has("database.host") # membership test
config.to_dict() # plain dict
print(config) # same grouped, masked display
Unlike EnvConfigLoader, YamlConfigLoader is instantiated with a path and runs a
__post_init__ hook where you shape the raw YAML into typed attributes.
Secret masking
Masking is a safety feature, applied wherever a config is displayed. A field is masked when its name contains any of:
SECRET API_KEY PASSWORD TOKEN CREDENTIAL
SECRET_API_KEY = "sk_live_abc123xyz789" # shown as 'sk_…89 (hidden)'
PASSWORD = "short" # shown as '***hidden***' (≤ 6 chars)
Masking applies to the display. to_dict() returns raw values (so you can read them);
use to_dict(mask=True) when logging the whole config. Extend the keyword set per class
(extend only, never narrow):
class MyConfig(EnvConfigLoader):
_sensitive_keywords = EnvConfigLoader._sensitive_keywords + ("PRIVATE_KEY",)
Display formats
print(config) always uses the boxed format by default, but render() can produce four more —
table, json, dotenv, flat — all masked by default. The call is class-level on
EnvConfigLoader/ConfigBase, instance-level on YamlConfigLoader (it mirrors the existing
__repr__ split):
AppConfig.render(fmt="table") # class-level — env/class configs
config.render(fmt="json") # instance-level — YAML configs
AppConfig.render(fmt="json")
# {
# "DATABASE_HOST": "localhost",
# "SECRET_API_KEY": "sk_…89 (hidden)"
# }
AppConfig.render(fmt="dotenv")
# DATABASE_HOST=localhost
# SECRET_API_KEY="sk_…89 (hidden)"
AppConfig.render(fmt="table", mask=False) # deliberate raw dump — never log this
Set a class's default output format once with _display_format (falls back to "boxed"):
class MyConfig(EnvConfigLoader):
_display_format = "flat" # now print(MyConfig) renders flat instead of boxed
fmt accepts one of configplusplus.DISPLAY_FORMATS — "boxed", "table",
"json", "dotenv", "flat" — and raises ValueError on anything else.
Custom validation
class APIConfig(EnvConfigLoader):
PORT = env("PORT", cast=int, default=8000)
@classmethod
def validate(cls) -> None:
super().validate() # always call super().validate()
if not (1024 <= cls.PORT <= 65535):
raise RuntimeError("PORT out of range")
APIConfig.validate()
Architecture
graph TD
Meta["ConfigMeta (metaclass)<br/>display · grouping · masking"]
Base["ConfigBase<br/>re-dispatches __repr__ to the metaclass"]
Env["EnvConfigLoader<br/>static · body runs at import time"]
Yaml["YamlConfigLoader<br/>instance · __post_init__ hook"]
Meta --> Base
Base --> Env
Meta -. "duplicates mask + render" .-> Yaml
The display lives on the metaclass, which is why print(MyConfig) and MyConfig.render(...)
work on the class with no instance. YamlConfigLoader intentionally re-implements masking and
render() so it can display instances the same way — call it as config.render(...).
Public API
| Symbol | Kind | Purpose |
|---|---|---|
EnvConfigLoader |
class | Static, class-based config read from environment variables |
YamlConfigLoader |
class | Instance-based config read from a YAML file |
ConfigBase |
class | Base for custom loaders; delegates display to ConfigMeta |
ConfigMeta |
metaclass | Owns to_dict, grouping, masking and render (class-level) |
env |
function | Read one variable with casting / default / required |
env_optional |
function | env(..., required=False) shorthand |
env_list |
function | Read a delimited variable as a typed list |
safe_load_envs |
function | Load .env file(s) from a path or directory, with logging |
The package ships a PEP 561 py.typed marker — its type hints are visible to downstream
type-checkers.
Documentation
| Guide | Contents |
|---|---|
| Installation | Install options and requirements |
| Usage | Full walkthrough of every feature |
| Reference | Concise API cheat-sheet |
examples/ |
Runnable end-to-end scripts |
Project layout
src/configplusplus/
├── __init__.py public API + __all__
├── base.py ConfigMeta (display, grouping, masking) + ConfigBase
├── env_loader.py EnvConfigLoader — static, import-time evaluation
├── yaml_loader.py YamlConfigLoader — instance, __post_init__, dot-notation get()
└── utils.py env() · env_optional() · safe_load_envs()
Development
poetry install # editable install (src layout)
poetry run pytest # tests + coverage
poetry run black src/ tests/ examples/ # formatting (checked in CI)
poetry run ruff check src/ tests/ examples/ # linting
poetry run mypy src/ # type checking
Releases are automated. Commit with Conventional Commits
(fix:, feat:, feat!:); merging to main lets release-please open a version-bump PR,
and merging that PR tags the release and publishes to PyPI via Trusted Publishing (OIDC).
License
MIT © Florian BARRE
Metadata
Release files for configplusplus 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| configplusplus-0.4.0.tar.gz | 18.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| configplusplus-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.0 kB
Release files / configplusplus-0.4.0.tar.gz
| Download URL | configplusplus-0.4.0.tar.gz |
|---|---|
| Size | 18.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82691a16d4ceee0b5d2472b56304468ca5a6bf545075ebc030aeb092735e5ebd
|
|
BLAKE2b-256 checksum How to use checksums |
c4b62c44434bbdf2d3e46e1be4b891e407a513ec749db05c81d34ed0cb167944
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / configplusplus-0.4.0-py3-none-any.whl
| Download URL | configplusplus-0.4.0-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f6c05928f86e63da5b81c55a6650341f05a8dd38bfcaddf589e5c6ae4e5a2609
|
|
BLAKE2b-256 checksum How to use checksums |
71d4e406bef2ae877b1313b9707de292a3eb9f37e1d8b02eb70c2eff1bb44a75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log