Skip to main content

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.

PyPI Python CI CodeQL License

Declare a config class, read each field with env(...) or from a YAML file, and print it. You get a grouped, aligned, secret-masked view of exactly what your service is running with — the same on the class and on an instance.


Why ConfigPlusPlus

  • One obvious place for configuration. A config is a class; every UPPERCASE attribute is a field. No scattered os.getenv calls, 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, TOKEN or CREDENTIAL are 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_PORTDATABASE).

EnvConfigLoader bodies are evaluated at import time — the environment must already be populated when the class is defined. Load your .env before 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",)

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 + __repr__" .-> Yaml

The display lives on the metaclass, which is why print(MyConfig) works on the class with no instance. YamlConfigLoader intentionally re-implements masking so it can display instances the same way.


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 and masking
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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

configplusplus-0.3.0.tar.gz (15.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

configplusplus-0.3.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file configplusplus-0.3.0.tar.gz.

File metadata

  • Download URL: configplusplus-0.3.0.tar.gz
  • Upload date:
  • Size: 15.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for configplusplus-0.3.0.tar.gz
Algorithm Hash digest
SHA256 0d4a38a4f0fdb40cd3c7a22da2fc0489878e1d61eeae86b4e351609d74a3fb07
MD5 bc3639df536478b8ed7776735ce5d652
BLAKE2b-256 322210af6045545cf8a13a18ceecd035a29f426999311fad45653d3e7ac69838

See more details on using hashes here.

Provenance

The following attestation bundles were made for configplusplus-0.3.0.tar.gz:

Publisher: publish.yml on Florian-BARRE/ConfigPlusPlus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file configplusplus-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: configplusplus-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for configplusplus-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 38c2f610b357aa39a94c0a8884d86fd36822bec4c0bcef7b47bf59cca7c1fa98
MD5 f1213205b0886a9f596aec6da206d81b
BLAKE2b-256 2cbeef9ae52cd66407f160e349199216be845c5487b1aec69439645ef37a5af3

See more details on using hashes here.

Provenance

The following attestation bundles were made for configplusplus-0.3.0-py3-none-any.whl:

Publisher: publish.yml on Florian-BARRE/ConfigPlusPlus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 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