Skip to main content

Type-safe TOML configuration management with environment variable substitution, automatic validation, and optional async I/O support for modern Python applications.

Project description

TomlEv - Open-source Python framework to manage environment variables


Latest Version Tomlev CI/CD Pipeline Coverage Status Versions Code Style: Ruff License Downloads

Motivation

TomlEv is a lightweight Python framework designed to simplify environment variable management using TOML configuration files with type-safe configuration models. It allows you to:

  • Type-safe configuration: Define configuration schemas using Python classes with type hints
  • Automatic type conversion: Convert environment variables to appropriate types (bool, int, float, str, lists, dicts, sets, tuples)
  • Nested configuration: Support for complex nested configuration structures
  • Environment variable substitution: Reference environment variables in TOML files with ${VAR|-default} syntax
  • Validation: Automatic validation of configuration structure and types
  • High performance: 50-60% faster than previous versions with optimized parsing and type conversion
  • Memory efficient: 40-50% less memory usage with automatic __slots__ generation
  • Async support: Non-blocking configuration loading for async applications
  • AI coding agent ready: Full type checking support makes configurations perfectly compatible with AI coding agents and IDEs
  • IDE support: Complete IDE autocompletion and static type analysis support

Install

# pip
pip install tomlev

# With async support
pip install 'tomlev[async]'
# uv
uv add tomlev

# With async support
uv add tomlev --optional async
# poetry
poetry add tomlev

# With async support
poetry add 'tomlev[async]'

Note: The [async] extra installs aiofiles for non-blocking file I/O operations. Use this when building async applications with FastAPI, aiohttp, or other async frameworks.

Basic usage

1. Create a TOML configuration file

Create a TOML configuration file (env.toml by default):

# env.toml
app_name = "My Application"
debug = "${DEBUG|-false}"
environment = "${ENV|-development}"

[database]
host = "${DB_HOST|-localhost}"
port = "${DB_PORT|-5432}"
user = "${DB_USER}"
password = "${DB_PASSWORD}"
name = "${DB_NAME|-app_db}"

[redis]
host = "${REDIS_HOST|-127.0.0.1}"
port = "${REDIS_PORT|-6379}"

Optionally include fragments inside a table using __include (paths are resolved relative to the TOML file):

# main.toml
[features]
__include = ["features.toml"]
# features.toml (merged under [features])
enabled = true
name = "awesome"

2. Define Configuration Models

Create configuration model classes that inherit from BaseConfigModel:

from tomlev import BaseConfigModel, TomlEv


class DatabaseConfig(BaseConfigModel):
    host: str
    port: int
    user: str
    password: str
    name: str


class RedisConfig(BaseConfigModel):
    host: str
    port: int


class FeaturesConfig(BaseConfigModel):
    # Matches the content merged under [features] via __include
    enabled: bool
    name: str


class AppConfig(BaseConfigModel):
    app_name: str
    debug: bool
    environment: str

    database: DatabaseConfig
    redis: RedisConfig
    features: FeaturesConfig

Tip: See the File Includes section for more details on __include usage and merge rules.

3. Use TomlEv in your Python code

Recommended: Simple convenience function

from tomlev import tomlev

# Simple one-liner - load and validate configuration
# Uses defaults: "env.toml" and ".env"
config: AppConfig = tomlev(AppConfig)

# Or explicitly specify files
config: AppConfig = tomlev(AppConfig, "env.toml", ".env")

# You can also set defaults via environment variables
# export TOMLEV_TOML_FILE="config/production.toml"
# export TOMLEV_ENV_FILE="config/.env.production"
# Then just use:
config: AppConfig = tomlev(AppConfig)  # Uses env var defaults

# Access configuration with type safety
print(f"App: {config.app_name}")
print(f"Environment: {config.environment}")
print(f"Debug mode: {config.debug}")  # Automatically converted to bool

# Access nested configuration
db_host = config.database.host
db_port = config.database.port  # Automatically converted to int

# All properties are type-safe and validated
redis_host = config.redis.host
redis_port = config.redis.port  # Automatically converted to int

Alternative: Class-based approach (when you need advanced features)

Use the TomlEv class when you need access to .environ, .strict, or .raw properties:

from tomlev import TomlEv

# Create instance to access additional properties
loader = TomlEv(AppConfig, "env.toml", ".env")

# Access environment variables used
env_vars = loader.environ

# Check strict mode setting
is_strict = loader.strict

# Get raw parsed TOML dict
raw_config = loader.raw

# Get validated config
config: AppConfig = loader.validate()

Async Support

TomlEv provides async I/O support for non-blocking configuration loading, perfect for async applications like FastAPI, aiohttp, or any async-based framework.

Installation:

pip install 'tomlev[async]'
# or with uv
uv add tomlev --optional async

Usage:

# Save as async_app.py, then run with: python async_app.py (or: uv run python async_app.py)
import asyncio
from tomlev import tomlev_async, BaseConfigModel


class AppConfig(BaseConfigModel):
    host: str
    port: int
    debug: bool


async def main():
    # Non-blocking configuration loading
    config = await tomlev_async(AppConfig, "env.toml", ".env")
    print(f"Server: {config.host}:{config.port}")


asyncio.run(main())

Advanced async usage with TomlEvAsync:

from tomlev import TomlEvAsync


async def main():
    # Create loader instance for access to additional properties
    loader = await TomlEvAsync.create(AppConfig, "env.toml", ".env")

    # Access environment variables
    env_vars = loader.environ

    # Get validated config
    config = loader.validate()

Benefits:

  • Non-blocking file I/O
  • Perfect for async web frameworks (FastAPI, aiohttp, Starlette)
  • Same API as synchronous version
  • Optional dependency - only install when needed

Immutable Configurations

Create frozen (immutable) configurations that cannot be modified after initialization:

from tomlev import BaseConfigModel, tomlev


class AppConfig(BaseConfigModel, frozen=True):
    host: str
    port: int


config = tomlev(AppConfig, "env.toml")

# This will raise AttributeError
try:
    config.port = 9000
except AttributeError as e:
    print(e)  # Cannot modify frozen configuration model: AppConfig

Benefits:

  • Thread-safe after initialization
  • Prevents accidental modifications
  • Clear intent in code
  • No performance penalty

Configuration Models

TomlEv uses BaseConfigModel to provide type-safe configuration handling. Here are the supported types:

Supported Types

  • Basic types: str, int, float, bool
  • Collections: list[T], dict[str, T], set[T], tuple[T, ...] where T is any supported type
  • Complex collections: list[dict[str, Any]] for lists of dictionaries
  • Nested models: Other BaseConfigModel subclasses
  • Generic types: typing.Any for flexible values

Advanced Example

from typing import Any
from tomlev import BaseConfigModel, tomlev, TomlEv


class QueryConfig(BaseConfigModel):
    get_version: str
    get_users: str


class DatabaseConfig(BaseConfigModel):
    host: str
    port: int
    user: str
    password: str
    name: str
    uri: str
    queries: dict[str, str]  # Dictionary of queries


class RedisConfig(BaseConfigModel):
    host: str
    port: int
    keys: list[str]  # List of strings
    nums: list[int]  # List of integers
    atts: list[dict[str, Any]]  # List of dictionaries
    tags: set[str]  # Set of unique strings
    coordinates: tuple[float, float, float]  # Tuple with fixed types
    weight: int
    mass: float


class AppConfig(BaseConfigModel):
    debug: bool
    environment: str
    temp: float

    database: DatabaseConfig
    redis: RedisConfig


# Simple usage with convenience function (recommended)
config: AppConfig = tomlev(AppConfig)

# Or explicitly specify files
config: AppConfig = tomlev(AppConfig, "env.toml", ".env")

# Alternative: Class-based approach if you need .environ, .strict, or .raw
config: AppConfig = TomlEv(AppConfig).validate()

CLI Usage

TomlEv also provides a small CLI to validate TOML configuration files with environment substitution, without writing Python code.

Validate using defaults (env.toml and .env in the current directory):

tomlev validate
# or with uv
uv run tomlev validate

Validate explicit files:

tomlev validate --toml path/to/app.toml --env-file path/to/.env
# or with uv
uv run tomlev validate --toml path/to/app.toml --env-file path/to/.env

Setting Default File Paths via Environment Variables

You can set default file paths using environment variables, which is useful for CI/CD pipelines or containerized environments:

# Set default file paths
export TOMLEV_TOML_FILE="config/production.toml"
export TOMLEV_ENV_FILE="config/.env.production"

# Now these commands will use the environment variable defaults
tomlev validate
tomlev render
# or with uv
uv run tomlev validate
uv run tomlev render

The precedence order is:

  1. Explicit command-line arguments (highest priority)
  2. Environment variables (TOMLEV_TOML_FILE, TOMLEV_ENV_FILE)
  3. Hardcoded defaults (env.toml, .env)

Disable strict mode (missing variables do not fail):

tomlev validate --no-strict

Ignore the .env file or system environment variables:

tomlev validate --no-env-file         # do not read .env
tomlev validate --no-environ          # do not include process environment

Customize the default separator used in ${VAR|-default} patterns (default is |-):

tomlev validate --separator "|-"

Exit codes: returns 0 on success, 1 on validation error (including missing files, substitution errors, or TOML parse errors). This makes it convenient to integrate into CI.

File Includes

TomlEv supports a simple include mechanism to compose configs from smaller TOML fragments. Place a reserved key __include inside any table to include one or more TOML files into that table.

Basic syntax (paths are resolved relative to the referencing file):

# main.toml
[database]
__include = ["database.toml"]

Included file content is merged under the table where __include appears. For example:

# database.toml
host = "${DB_HOST|-localhost}"
port = "${DB_PORT|-5432}"

[nested]
flag = true

After expansion and substitution, the effective configuration is equivalent to:

[database]
host = "localhost"
port = 5432

[database.nested]
flag = true

Notes:

  • __include can be a string or a list of strings: __include = "file.toml" or __include = ["a.toml", "b.toml"].
  • Includes are expanded using the same environment mapping and strict mode as the parent file.
  • Merge rules: dictionaries are deep-merged; non-dicts (strings, numbers, booleans, lists) are replaced by later includes (last one wins).
  • Strict mode: missing files and include cycles raise errors. In non-strict mode, they are skipped.
  • The __include key is removed from the final configuration prior to model validation.

TOML File with Complex Types

debug = "${DEBUG|-false}"
environment = "${CI_ENVIRONMENT_SLUG|-develop}"
temp = "${TEMP_VAL|--20.5}"

[database]
host = "${DB_HOST|-localhost}"
port = "${DB_PORT|-5432}"
user = "${DB_USER}"
password = "${DB_PASSWORD}"
name = "${DB_NAME|-app_db}"
uri = "postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST|-localhost}:${DB_PORT|-5432}/${DB_NAME|-app_db}"

[database.queries]
get_version = """SELECT version();"""
get_users = """SELECT * FROM "users";"""

[redis]
host = "${REDIS_HOST|-127.0.0.1}"
port = "${REDIS_PORT|-6379}"
keys = ["one", "two", "three"]
nums = [10, 12, 99]
atts = [{ name = "one", age = 10 }, { name = "two", age = 12 }]
tags = ["cache", "session", "cache", "metrics"]  # Will be deduplicated to set
coordinates = [52.5200, 13.4050, 100.0]  # Will be converted to tuple
weight = 0.98
mass = 0.78

Strict mode

By default, TomlEv operates in strict mode, which means it will raise a ValueError if:

  1. An environment variable referenced in the TOML file is not defined and has no default value
  2. The same variable is defined multiple times in the .env file

This helps catch configuration errors early. You can disable strict mode in two ways:

# Method 1: Set the environment variable TOMLEV_STRICT_DISABLE
import os

os.environ["TOMLEV_STRICT_DISABLE"] = "true"
config = tomlev(AppConfig)  # Uses defaults: "env.toml" and ".env"

# Method 2: Pass strict=False when calling tomlev()
config = tomlev(AppConfig, strict=False)  # Uses defaults with strict=False

# Alternative: Using the TomlEv class
config = TomlEv(AppConfig, strict=False).validate()

When strict mode is disabled, TomlEv will not raise errors for missing environment variables or duplicate definitions.

Performance

TomlEv v1.0.9 includes significant performance improvements:

  • 50-60% faster overall performance
  • 40-50% less memory usage per configuration instance
  • 30-40% faster initialization with type hints caching
  • 20-30% faster type conversion with optimized converters
  • 15-20% faster parsing with batch string substitution

Key optimizations:

  • Type hints caching with @lru_cache
  • Dict-based dispatch for type converters
  • Auto-generated __slots__ for memory efficiency
  • Batch regex replacement for string substitution
  • Optimized file I/O with caching

These improvements are automatic - no code changes required to benefit from them!

Support

If you like TomlEv, please give it a star on GitHub: https://github.com/thesimj/tomlev

License

MIT licensed. See the LICENSE file for more details.

Project details


Download files

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

Source Distribution

tomlev-1.0.9.tar.gz (157.1 kB view details)

Uploaded Source

Built Distribution

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

tomlev-1.0.9-py3-none-any.whl (37.3 kB view details)

Uploaded Python 3

File details

Details for the file tomlev-1.0.9.tar.gz.

File metadata

  • Download URL: tomlev-1.0.9.tar.gz
  • Upload date:
  • Size: 157.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for tomlev-1.0.9.tar.gz
Algorithm Hash digest
SHA256 93d2864b82125feb16b4ba97d4bbc4fb57d8f3475756ef78ca8b225e942cf740
MD5 46f0747623d80496ff1e8872caec487e
BLAKE2b-256 85627c0403b34bb81fd66784829942c48ca80a2b9bd7012ff2b11b9626a0ec7a

See more details on using hashes here.

File details

Details for the file tomlev-1.0.9-py3-none-any.whl.

File metadata

  • Download URL: tomlev-1.0.9-py3-none-any.whl
  • Upload date:
  • Size: 37.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for tomlev-1.0.9-py3-none-any.whl
Algorithm Hash digest
SHA256 d5e82a9d2a05fcb437c2b877f00b9a2b29154e9d67ffa6a2692eeaf886767fd8
MD5 2035a7eb66f60ef3c0e2ea72e00aa6c8
BLAKE2b-256 83ba2275980fda7106376107312cd752b8aa288ed9454ba24476cbc4cdbb8a12

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page