Skip to main content

smartenv

PyPI Python 3.8+ License: MIT CI Coverage floor: 90%

One library to load, validate, and manage all your config — everywhere.

Combine environment variables, local files, and cloud secrets in one explicit priority chain. Declare your types once, catch configuration errors at startup, and access real Python values. The core uses only the standard library; optional integrations load when you use them.

Version 1.0 adds atomic reload rollback, explicit defaults, source provenance, expiring cloud caches, and safer diagnostics. Python 3.8–3.14 is covered by the CI configuration, with Windows and macOS checks on Python 3.12. The coverage badge describes the enforced minimum; the CI and PyPI badges report their respective services.

Upgrading from 0.1? Read the migration guide.

Why smartenv?

smartenv is for applications that need several configuration sources, an explicit schema, and the same loading rules in development and production.

Feature smartenv python-dotenv dynaconf environs raw os.environ
Multi-source fallback Ordered files, environment, and cloud Environment + dotenv; merge dictionaries yourself Layered files, environment, and loaders Environment + dotenv Write your own
Type casting Schema types and callables Strings Automatic parsing and converters Typed parser methods Write your own
Startup validation Required keys, types, custom validators Write your own Validators Parser validation; seal() collects errors Write your own
Cloud secrets AWS, GCP, Azure extras Custom integration Vault / Redis extras; custom loaders Custom integration; secret files supported Custom integration
Hot reload File watcher extra Call loading code again Reload API / fresh settings reads Call loading code again No file loading
CLI tools Validate, generate, list, check-source Optional CLI extra Built-in CLI No dedicated config CLI No config CLI
Zero deps Yes, core Yes, core Yes, core with vendored helpers No Standard library

“Write your own” and “custom integration” mean application code is needed. The comparison describes documented built-in features, not what each library can support through extensions. Sources: python-dotenv documentation, Dynaconf documentation, Dynaconf fresh settings, Dynaconf dependencies, environs documentation, environs dependencies, and os.environ.

Install

The PyPI distribution is named smartenv-config. Python imports and the CLI remain smartenv. After publication, install it with:

pip install smartenv-config
pip install "smartenv-config[yaml,aws]"
pip install "smartenv-config[all]"

To use this checkout now, run python -m pip install -e . from the project directory.

Extra Enables
yaml YAML files through PyYAML
toml TOML on Python 3.8–3.10 through tomli; Python 3.11+ uses tomllib
watch File watching through watchdog
aws AWS Secrets Manager through boto3
gcp Google Secret Manager
azure Azure Key Vault and DefaultAzureCredential
pydantic Pydantic models as schema declarations
all Every optional integration above
dev Test, coverage, formatting, linting, and type-checking tools

Quick Start

Create a .env file in your working directory:

DATABASE_URL=sqlite:///app.db
PORT=8000
DEBUG=false
MAX_RETRIES=3

Then run this Python example. Existing environment variables take priority over the file:

from smartenv import Env

schema = {
    "DATABASE_URL": str,
    "PORT": int,
    "DEBUG": bool,
    "MAX_RETRIES": int,
}
env = Env(
    schema=schema,
    sources=["os", ".env"],
    required=["DATABASE_URL", "PORT"],
    strict=True,
)
print(env.PORT)                    # 8000 (int)
print(env["DEBUG"])                # False (bool)
print(env.get("MAX_RETRIES", 3))    # 3
print(env.valid)                   # True

Only keys declared in schema are exposed. Keys become required only when named in required; Optional[int] describes an allowed value type, not presence. Env(schema) defaults to the os source. Use env.dict() for a snapshot and env.json() for JSON serialization of JSON-compatible values. These exports contain the actual values. Use env.dict(redact=True) or env.json(redact=True) for key-based masking when inspecting configuration. See Security for the limits of that masking.

Explicit defaults are cast and validated like source values, with the lowest priority:

env = Env(
    {"PORT": int, "DEBUG": bool},
    sources=["os"],
    defaults={"PORT": 8000, "DEBUG": False},
    strict=True,
)
print(env.get_source("PORT"))  # "os" if set there, otherwise "defaults"

get_source(key) returns the winning source's name. A missing resolved key raises MissingKeyError, which is also an AttributeError and KeyError, so hasattr, getattr(env, "MISSING", fallback), and standard missing-key handling work normally. Use brackets for keys that overlap API names such as env["sources"] or contain dashes. Returned builtin containers are copied; custom objects returned by casters should be treated as immutable.

Source Fallback Chain

Sources have left-to-right priority for each key. The first source defining a key wins; later sources fill gaps. An empty value still counts as defined, so it does not fall through.

sources=["os", ".env", "defaults.json"]

highest priority                                  lowest priority
   os.environ         .env           defaults.json
  PORT=9000        PORT=8000           PORT=3000
                   DEBUG=true         DEBUG=false
                                      MAX_RETRIES=3
       |                |                  |
       +----------------+------------------+
                        v
           PORT=9000, DEBUG=True, MAX_RETRIES=3

Built-in source strings are os, .env paths, .json, .toml, .yaml / .yml, aws_secrets, gcp_secrets, and azure_secrets. Named dotenv variants such as .env.production, pathlib.Path instances, and source objects are also accepted. Custom BaseSource subclasses need a name property and a load() method returning a flat mapping of key names to string values.

JSON, TOML, and YAML mappings are flattened into uppercase keys separated by __. For example, {"database": {"host": "localhost"}} becomes DATABASE__HOST=localhost. Arrays become JSON text and can be cast with List[T].

By default, missing files produce warnings; with strict=True they raise MissingSourceError. A malformed source raises SourceLoadError. All sources are loaded, even when earlier sources already supply every schema key: this is value precedence, not an outage failover mechanism. Reading sources does not mutate os.environ.

Type Casting

Use typing generics for examples that also run on Python 3.8:

Schema type Raw input Result
str "hello" "hello"
int "8080" 8080
float "0.75" 0.75
bool "yes" / "off" True / False
bytes "hello" b"hello" (UTF-8)
list / List[str] "red, blue" ["red", "blue"]
List[int] "[1, 2, 3]" [1, 2, 3]
dict '{"enabled": true}' {"enabled": True}
Dict[str, int] '{"workers": "4"}' {"workers": 4}
Optional[int] "null" / "42" None / 42
Union[int, str] "42" / "auto" 42 / "auto"
Literal["dev", "prod"] "prod" "prod"; other choices fail
type(None) "none" None
Any / object / None Any value Passed through unchanged
Callable, e.g. Decimal or an enum class Text accepted by the callable Callable result

Boolean text accepts true, 1, yes, on, false, 0, no, and off, ignoring case and surrounding whitespace. Optional targets recognize empty text, null, and none as None. Comma-separated lists trim entries and omit empty entries. Unions try members in declaration order. A direct cast(value, type_hint) call raises CastError on failure.

Missing keys remain absent, even when their type is Optional. Empty values are ignored by validation unless required; Env preserves an empty Optional value as None. Pydantic model schemas contribute field annotations only; model defaults and Pydantic validators are not executed. Supply required and validators to Env explicitly.

Validation

Use strict=True to reject invalid startup configuration and report every validation problem in one exception:

from smartenv import Env, ValidationError

try:
    Env(
        schema={"DATABASE_URL": str, "API_KEY": str},
        sources=[],
        required=["DATABASE_URL", "API_KEY"],
        strict=True,
    )
except ValidationError as error:
    print(error)
environment validation failed with 2 errors
errors:
  - missing required key 'DATABASE_URL'
  - missing required key 'API_KEY'

Without strict mode, inspect env.valid, env.errors, and env.warnings. Custom validators receive the cast value:

env = Env(
    {"PORT": int},
    sources=["os", ".env"],
    required=["PORT"],
    validators={"PORT": lambda value: 1 <= value <= 65535},
    strict=True,
)

A validator can return True or None for success, False for failure, or an explanatory string. Exceptions from validators become validation errors. Each schema value is cast once per load, and that result is passed to its validator and exposed by Env. Sensitive key names are masked in cast and validator display messages. Raw exception attributes, chained exceptions, and values under unrecognized names may still contain secrets; keep full exception reports out of public responses. See Security.

Cloud Secrets

Install the matching extra and configure credentials using the provider SDK's normal credential discovery. Cloud sources import their SDKs lazily and wrap provider failures in CloudAuthError, including a useful install hint when an SDK is missing.

AWS Secrets Manager — a JSON object supplies schema keys; plain text uses AWS_SECRET:

from smartenv import Env
from smartenv.sources import AwsSource

aws = AwsSource(secret_name="production/myapp", region="us-east-1", cache_ttl=300)
env = Env({"DATABASE_URL": str}, sources=["os", aws],
          required=["DATABASE_URL"], strict=True)

With no explicit arguments, AwsSource() reads AWS_SECRET_NAME and AWS_REGION; the region defaults to us-east-1. Both SecretString and UTF-8 SecretBinary payloads are supported; arbitrary binary secrets that are not UTF-8 text are rejected.

Google Secret Manager — decode a secret version as a JSON object or expose plain text as SECRET:

from smartenv import Env
from smartenv.sources import GcpSource

gcp = GcpSource(project_id="my-project", secret_id="myapp-config", version="latest")
env = Env({"DATABASE_URL": str}, sources=[gcp],
          required=["DATABASE_URL"], strict=True)

GCP_PROJECT_ID and GCP_SECRET_ID provide constructor fallbacks. JSON object payloads in AWS and GCP are flattened with the same rules as JSON files. Without an explicit secret ID or GCP_SECRET_ID, GCP lists and fetches the project's secrets. In this mode, plain payloads use each secret's uppercased ID; JSON payloads merge their flattened keys. Prefer explicit IDs to limit listing permissions and ambiguous keys.

Azure Key Vault — secret names become uppercase keys; values remain their stored text:

from smartenv import Env
from smartenv.sources import AzureSource

azure = AzureSource(vault_url="https://myvault.vault.azure.net",
                    secret_names=["database-url"])
env = Env({"DATABASE-URL": str}, sources=[azure],
          required=["DATABASE-URL"], strict=True)
database_url = env["DATABASE-URL"]

AzureSource() reads AZURE_VAULT_URL and uses DefaultAzureCredential. Omit secret_names to list and fetch all secrets; this also requires list access. Dashes in Azure secret names are preserved, so use bracket access for those keys.

Every cloud source supports await source.aload(), using a thread-backed wrapper around the synchronous SDK. Successful results are cached per instance by default; returned dictionaries are copies and failed fetches are not cached. Set cache=False to fetch on every load, or call source.cache_clear() before env.reload() to refresh cached secrets. Set cache_ttl to a positive number of seconds to expire cached results. Expiration triggers a fetch on the next load, not a background poll. The default cache_ttl=None caches indefinitely. Failed refreshes raise instead of silently serving expired secrets; strict Env reloads retain their previous configuration. SDK timeouts and retries follow the provider's configuration. An async wrapper does not make the SDK itself asynchronous.

Hot Reload

Install smartenv-config[watch], then enable file watching and keep the process running:

import time
from smartenv import Env

def on_reload(config: Env) -> None:
    print("New port:", config.PORT)

with Env(
    {"PORT": int},
    sources=[".env"],
    required=["PORT"],
    strict=True,
    hot_reload=True,
    on_reload=on_reload,
) as env:
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass

The daemon observer watches each file's parent directory. Reload starts after 0.5 seconds without another matching event, including editor saves that replace the file atomically. A single worker serializes reloads and callbacks. Reload reads all sources, validates and casts them, and atomically publishes the new configuration. The callback runs after a successful reload, on the worker thread; keep callbacks short and thread-safe. Cloud caches still apply. Use env.close() or the context manager to cancel pending reloads and wait for active work to finish. A callback may safely call env.close() itself. Without watchdog, hot_reload=True records a warning; check env.warnings and env.watcher. If strict validation or source loading fails, the previous values, provenance, and validation report remain intact. The watcher records a warning and skips the callback; a later valid save can recover. Direct env.reload() raises the error for the caller to inspect. Non-strict validation continues to publish its latest values and error report. Use env.dict() when several keys must come from the same snapshot: separate attribute reads can span a concurrent reload. Callback failures occur after the update and do not roll back a successfully published configuration.

CLI

The smartenv command is included in the base installation. A schema module defines a schema dictionary and optional required, validators, and defaults values:

# schema.py
schema = {"DATABASE_URL": str, "PORT": int}
required = ["DATABASE_URL"]
validators = {"PORT": lambda value: 1 <= value <= 65535}
defaults = {"PORT": 8000}

Schema files run as Python code when loaded. Use schema modules you trust. Install the yaml extra to include config.yaml in these examples. CLI defaults must name declared schema keys and are validated after source values take priority. Generated examples keep values blank, even when defaults are defined.

Validate sources against the schema. All validation errors are listed; success exits with status 0 and failure with status 1:

$ smartenv validate --schema schema.py --sources .env config.yaml
✓ Validation passed

Use --format json on validate or list for machine-readable output:

$ smartenv validate --schema schema.py --sources .env --format json
{"valid": true, "errors": [], "warnings": []}
$ python -m smartenv --version
smartenv 1.0.0

If DATABASE_URL is absent, the validation output includes:

✗ missing required key 'DATABASE_URL'

Generate an example with type and presence comments. Values are intentionally blank for the application owner to fill in:

$ smartenv generate-example --schema schema.py --output .env.example
✓ Wrote .env.example
# DATABASE_URL (str) [REQUIRED]
DATABASE_URL=
# PORT (int) [optional]
PORT=

Existing output files are protected. Pass --force to intentionally replace one.

List all source values without needing a schema. Earlier sources win. For a file with PORT=8000, DEBUG=false, and API_KEY set, representative output is:

$ smartenv list --sources .env config.yaml
API_KEY=*****
DEBUG=false
PORT=8000

Any key containing AUTH, CERT, CREDENTIAL, KEY, PASS, PRIVATE, SECRET, or TOKEN (case-insensitive) is masked as *****. Values under other names are printed as-is.

Check a source using environment-based provider configuration and credentials:

$ smartenv check-source --source aws_secrets
✓ Connected to aws_secrets

On a connection or loading error it prints ✗ Failed: ... and exits with status 1. Use smartenv --help or python -m smartenv --help for command usage. Invalid command syntax exits with status 2. The CLI loads every explicitly named source and fails when one cannot be loaded, including a missing file.

Framework Integration

FastAPI: create configuration once during application startup, then return it from a dependency. The dependency pattern follows the FastAPI settings guide.

from fastapi import Depends, FastAPI
from smartenv import Env

settings = Env(
    {"PORT": int, "DEBUG": bool},
    sources=["os", ".env"],
    required=["PORT"],
    strict=True,
)
app = FastAPI()

def get_settings() -> Env:
    return settings

@app.get("/health")
def health(config: Env = Depends(get_settings)) -> dict:
    return {"status": "ok", "debug": config.get("DEBUG", False)}

Django: put typed values into the existing settings.py module, which is how Django loads settings:

# settings.py (alongside the rest of your Django settings)
from pathlib import Path
from typing import List
from smartenv import Env

BASE_DIR = Path(__file__).resolve().parent.parent
config = Env(
    {"SECRET_KEY": str, "DEBUG": bool, "ALLOWED_HOSTS": List[str]},
    sources=["os", str(BASE_DIR / ".env")],
    required=["SECRET_KEY"],
    strict=True,
)
SECRET_KEY = config.SECRET_KEY
DEBUG = config.get("DEBUG", False)
ALLOWED_HOSTS = config.get("ALLOWED_HOSTS", ["localhost"])

Frameworks are installed separately. These examples use ordinary Python integration; smartenv does not require a framework plugin.

Contributing

Fork the repository, clone your fork, and create a branch for the change:

git switch -c feat/my-change
python -m pip install -e ".[dev,yaml,toml,watch,pydantic]"
python -m black smartenv tests scripts
python -m ruff check .
python -m mypy smartenv tests scripts
python -m pytest tests/ -v --tb=short --cov=smartenv

Read .clinerules and cline_todo.md, keep Python 3.8 compatibility, and add focused tests for behavior changes. Cloud tests use fake SDK clients and require no credentials. Use conventional commit messages and open a pull request describing the change and the checks you ran. CI covers Python 3.8 through 3.14 on Linux and Python 3.12 on Windows and macOS, with formatting, lint, strict type checks, doctests, a 90% coverage floor, and an isolated wheel installation check. See CONTRIBUTING.md.

Release maintainers can follow the release guide. The tagged release workflow runs CI and publishes verified distributions through PyPI Trusted Publishing; it does not use a stored PYPI_TOKEN. A local build does not publish the package.

License

MIT © 2026 smartenv contributors. See LICENSE.

Metadata

Release files for smartenv-config 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for smartenv-config 1.0.0
File Size Uploaded Provenance
smartenv_config-1.0.0.tar.gz 81.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for smartenv-config 1.0.0
File Interpreter ABI Platform
smartenv_config-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 137.4 kB

Release files / smartenv_config-1.0.0.tar.gz

Download URL smartenv_config-1.0.0.tar.gz
Size 81.4 kB
Tags Source
SHA-256 checksum
How to use checksums
399644c1526d9e93f2bc83a69bbf7f4d3590ff2370e201eaa05807e132301598
BLAKE2b-256 checksum
How to use checksums
c976f7cfb26e96647fde90ab54b4495e0337a75d372127e78d077574f0f7cada
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 22, 2026.

Transparency log

Release files / smartenv_config-1.0.0-py3-none-any.whl

Download URL smartenv_config-1.0.0-py3-none-any.whl
Size 56.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9e9e62a45c4926289a3601ea4d67001a0737987c80e58bd6c38829e883af9765
BLAKE2b-256 checksum
How to use checksums
c0d527e3053667961ca3217e14114623e6571c0ba9a94c711b8262ea6ea74c2a
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 22, 2026.

Transparency log

Security for smartenv-config 1.0.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for smartenv_config-1.0.0.tar.gz

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 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

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