Skip to main content

Smart environment loader with robust casting/normalization. Layers: decouple → dotenv → os.environ.

Project description

castenv

castenv is a smart environment loader with robust casting/normalization, that uses python-decouple (if installed), python-dotenv with a Fallback to os.environ

current_version = "v0.3.0"

  1. python-decouple (if installed)
  2. python-dotenv (if installed; auto-discovers .env, .env.local, .env.{env}, .env.{env}.local across cwd+parents)
  3. Fallback to os.environ

No per-call config — just castenv.get_env("KEY"), like os.environ.get but smarter.

Quick start

Install

pip install castenv
# Optional extras:
pip install "castenv[dotenv]"
pip install "castenv[decouple]"

Example

import castenv as env

# Optional one-time setup (else: auto cwd+parents):
# env.configure(search_dirs=[BASE_DIR], env_name="prod")

DATABASE_URL = env.get_env("DATABASE_URL", None)
DEBUG = env.env_bool("DEBUG", False)
DEBUG2 = env.get_env("DEBUG2", True)
TIMEOUT_SECS = env.get_env("TIMEOUT", "1m30s")  # -> seconds float (90.0)
TIMEOUT_HOURS = env.get_env("HOURS", "2d1h")  # -> seconds float (176400.0)
CACHE_BYTES = env.get_env("CACHE", "256MB")     # -> bytes int (256000000)
ALLOWED = env.get_env(
    "ALLOWED_HOSTS",
    "localhost,127.0.0.1",
    normalize_kwargs={"parse_lists": True}
)
PORT   = env.get_env("PORT", 8080)     # -> 8080 (int)
OPTS   = env.get_env("OPTS", {"x": 1}) # -> {"x": 1} (dict)
SCALE = env.get_env("SCALE", "50%", normalize_kwargs={"percent_mode": "fraction"})
TAX   = env.get_env("PORT", 15.5)     # -> 15.5 (float)

print("DATABASE_URL:", DATABASE_URL)
print("DEBUG (bool) [env_bool]:", DEBUG)
print("DEBUG2 (bool) [get_env]:", DEBUG2)
print("TIMEOUT (secs):", TIMEOUT_SECS)
print("TIMEOUT_HOURS (secs):", TIMEOUT_HOURS)
print("CACHE (bytes):", CACHE_BYTES)
print("ALLOWED_HOSTS (list):", ALLOWED)
print("PORT (int):", PORT)
print("OPTS (dict):", OPTS)
print("SCALE (float) [0-1]:", SCALE)
print("TAX (float):", TAX)

API Reference

Main Functions

get_env(key, default=None, *, normalize_kwargs=None)

Retrieves and normalizes an environment variable with automatic type casting.

Parameters:

  • key (str): Environment variable name
  • default (Any): Default value if key not found (default: None)
  • normalize_kwargs (dict, optional): Additional normalization options (see below)

Returns: Normalized value based on content and settings

env_bool(key, default=None, **kwargs)

Retrieves environment variable as boolean.

Boolean values recognized:

  • True: "true", "yes", "y", "on", "1"
  • False: "false", "no", "n", "off", "0"

env_int(key, default=None, **kwargs)

Retrieves environment variable as integer.

env_float(key, default=None, **kwargs)

Retrieves environment variable as float.

env_str(key, default=None, **kwargs)

Retrieves environment variable as string.

env_list(key, default=None, *, separators=(",",), **kwargs)

Retrieves environment variable as list with custom separators.

Parameters:

  • separators: Tuple of separator strings (default: (",",))

get_all(keys, defaults=None, *, normalize_kwargs=None)

Retrieves multiple environment variables at once.

Parameters:

  • keys: Iterable of key names
  • defaults: Dict of default values per key
  • normalize_kwargs: Normalization options for all keys

normalize_config(obj, **normalize_kwargs)

Recursively normalizes config structures (dicts, lists, strings) with environment variable expansion and automatic type casting.

This is useful for loading structured config from environment variables, where you want:

  1. Full support for ${VAR}, ${VAR:-default}, and $VAR environment variable references
  2. Layered source precedence (decouple → dotenv → os.environ) for all variable lookups
  3. Automatic type casting of expanded strings (booleans, numbers, JSON, durations, etc.)
  4. Recursive processing of nested dicts and lists

Parameters:

  • obj (Any): The object to process (dict, list, string, or other type)
  • **kwargs: All normalization options from normalize() (see Normalization Parameters below)

Returns: The same structure with env vars expanded and types automatically cast

Key Differences from os.path.expandvars():

  • Respects castenv's configured dotenv search and precedence rules
  • Supports python-decouple if installed
  • Handles missing variables gracefully (returns empty string by default, or uses ${VAR:-default} syntax for defaults)

Example:

import os
import castenv as env

# Set environment variables
os.environ['DB_HOST'] = 'localhost'
os.environ['DB_PORT'] = '5432'
os.environ['DB_DEBUG'] = 'true'
os.environ['ALLOWED_HOSTS'] = 'localhost,127.0.0.1,192.168.1.1'
os.environ['TIMEOUTS'] = '30s,60s,120s'

config = {
    "database": {
        "host": "${DB_HOST}",
        "port": "${DB_PORT}",
        "debug": "${DB_DEBUG}",
        "max_connections": "${MAX_CONNECTIONS:-100}",  # Uses default if not set
    },
    "hosts": "${ALLOWED_HOSTS}",
    "timeouts": "${TIMEOUTS}",
}

result = env.normalize_config(config)
# Returns:
# {
#     "database": {
#         "host": "localhost",
#         "port": 5432,              # int (not string)
#         "debug": True,             # bool (not string)
#         "max_connections": 100,    # int (from default)
#     },
#     "hosts": ["localhost", "127.0.0.1", "192.168.1.1"],  # list
#     "timeouts": [30.0, 60.0, 120.0],  # list of floats (seconds)
# }

Normalization & Casting

Automatic Type Detection

castenv automatically detects and converts values based on their content:

1. None/Null Values

Converts to Python None:

  • Empty string: ""
  • Null literals: "null", "none", "nil", "undefined"
env.get_env("MISSING", "")  # -> None (if coerce_empty_to_none=True)
env.get_env("NULL_VAL", "null")  # -> None

2. Boolean Conversion

Automatically detects boolean strings (case-insensitive):

True values: "true", "yes", "y", "on", "1"
False values: "false", "no", "n", "off", "0"

env.get_env("DEBUG", "true")   # -> True
env.env_bool("ENABLED", "no")  # -> False

3. Number Parsing

Integers:

  • Decimal: "42"42
  • Hexadecimal: "0xFF"255
  • Binary: "0b1010"10
  • Octal: "0o10"8

Floats:

  • Standard: "3.14"3.14
  • Scientific notation: "1e-3"0.001
env.get_env("HEX_COLOR", "0xFF00FF")  # -> 16711935
env.get_env("RATIO", "1.5e-2")        # -> 0.015

4. Duration Parsing

Converts human-readable durations to seconds (float):

Supported units:

  • ns - nanoseconds
  • us, µs - microseconds
  • ms - milliseconds
  • s - seconds
  • m - minutes
  • h - hours
  • d - days
  • w - weeks

Examples:

env.get_env("TIMEOUT", "1m30s")     # -> 90.0
env.get_env("CACHE_TTL", "2d1h")    # -> 176400.0
env.get_env("DELAY", "500ms")       # -> 0.5
env.get_env("WEEK", "1w")           # -> 604800.0

5. Byte Size Parsing

Converts size strings to bytes (int):

IEC Units (powers of 1024):

  • b, B - bytes
  • k, kb, kib, KiB - kibibytes
  • m, mb, mib, MiB - mebibytes
  • g, gb, gib, GiB - gibibytes
  • t, tb, tib, TiB - tebibytes

SI Units (powers of 1000, uppercase trigger):

  • KB - kilobytes (1000)
  • MB - megabytes (1000²)
  • GB - gigabytes (1000³)
  • TB - terabytes (1000⁴)
env.get_env("CACHE", "256MB")       # -> 256000000 (SI: 256 * 1000²)
env.get_env("BUFFER", "1MiB")       # -> 1048576 (IEC: 1024²)
env.get_env("DISK", "500gb")        # -> 536870912000 (IEC by default)

6. Percentage Conversion

Handles percentage values with configurable output:

Modes (via percent_mode):

  • "none" - Returns as string (default): "50%""50%"
  • "number" - Returns numeric value: "50%"50.0
  • "fraction" - Returns decimal fraction: "50%"0.5
env.get_env("TAX", "15%", normalize_kwargs={"percent_mode": "number"})     # -> 15.0
env.get_env("SCALE", "50%", normalize_kwargs={"percent_mode": "fraction"}) # -> 0.5
env.get_env("RAW", "25%")  # -> "25%" (default)

7. JSON Parsing

Automatically parses JSON objects and arrays:

env.get_env("CONFIG", '{"key": "value", "count": 10}')  # -> {'key': 'value', 'count': 10}
env.get_env("ITEMS", '["a", "b", "c"]')                 # -> ['a', 'b', 'c']
env.get_env("QUOTED", '"{\\"k\\": \\"v\\"}"')           # -> {'k': 'v'}

8. List Parsing

Splits comma-separated values (or custom separators) into lists:

Default separator: ,

env.get_env("HOSTS", "localhost,127.0.0.1", normalize_kwargs={"parse_lists": True})
# -> ['localhost', '127.0.0.1']

env.env_list("PORTS", "8000;8001;8002", separators=(";",))
# -> ['8000', '8001', '8002']

# Recursive normalization on list items
env.get_env("MIXED", "true,42,3.14", normalize_kwargs={"parse_lists": True})
# -> [True, 42, 3.14]

9. Environment Variable Interpolation

Expands ${VAR} and $VAR references:

# With BASE_URL="https://api.example.com"
env.get_env("API_URL", "${BASE_URL}/v1")        # -> "https://api.example.com/v1"
env.get_env("FULL_URL", "${BASE:-http://localhost}/api")  # -> Uses default if BASE not set
env.get_env("PATH", "$HOME/config")             # -> Expands $HOME

10. Path Expansion

Expands ~ to user's home directory:

env.get_env("LOG_FILE", "~/app/logs/app.log")  # -> "/home/user/app/logs/app.log"

11. Quote Handling

Strips matching quotes and unescapes common sequences:

Escape sequences supported:

  • \\\
  • \""
  • \''
  • \n → newline
  • \r → carriage return
  • \t → tab
  • \b → backspace
  • \f → form feed
  • \0 → null byte
env.get_env("MSG", '"Hello\\nWorld"')  # -> "Hello\nWorld" (with actual newline)
env.get_env("PATH", "'C:\\\\Users\\\\App'")  # -> "C:\Users\App"

Normalization Parameters

The normalize_kwargs dictionary accepts the following parameters:

Parameter Type Default Description
coerce_empty_to_none bool True Convert empty strings to None
coerce_null_strings bool True Convert "null"/"none" to None
parse_booleans bool True Parse boolean strings
parse_numbers bool True Parse numeric strings
parse_json bool True Parse JSON objects/arrays
parse_lists bool True Parse comma-separated lists
list_separators tuple (",",) Separators for list parsing
strip_quotes bool True Remove matching quotes
unescape_in_quotes bool True Unescape sequences in quotes
interpolate_env bool True Expand ${VAR} references
expand_user bool True Expand ~ to home directory
parse_duration bool True Parse duration strings
parse_bytesize bool True Parse byte size strings
percent_mode str "none" Percentage mode: "none", "number", or "fraction"
lowercase_strings bool False Convert final strings to lowercase
enum iterable None Validate value is in allowed set

Examples with Custom Parameters

# Disable list parsing
env.get_env("CSV", "a,b,c", normalize_kwargs={"parse_lists": False})  # -> "a,b,c"

# Custom list separator
env.get_env("ITEMS", "x|y|z", normalize_kwargs={"parse_lists": True, "list_separators": ("|",)})  # -> ['x', 'y', 'z']

# Enum validation
env.get_env("ENV", "dev", normalize_kwargs={"enum": ["dev", "staging", "prod"]})  # -> "dev"
# Raises ValueError if value not in enum

# Lowercase strings
env.get_env("NAME", "MyApp", normalize_kwargs={"lowercase_strings": True})  # -> "myapp"

# Disable specific parsers
env.get_env("RAW", "true", normalize_kwargs={"parse_booleans": False})  # -> "true" (string)

Configuration

Global Configuration

Configure castenv once at startup:

from pathlib import Path
import castenv as env

env.configure(
    search_dirs=[Path("/app/config")],     # Where to look for .env files
    env_name="production",                  # Use .env.production files
    filenames=[".env", ".env.local"],       # Custom file names
    stop_at_first_found_dir=True,          # Stop at first dir with .env
    prefer_os_over_dotenv=True,            # OS env vars take precedence
    use_decouple_if_available=True         # Use python-decouple if installed
)

Temporary Configuration (Testing)

Use context manager for temporary overrides:

with env.using(search_dirs=[Path("tests/fixtures")], env_name="test"):
    # Temporary configuration active here
    value = env.get_env("TEST_VAR")
# Original configuration restored

Testing overrides

from pathlib import Path
import castenv as env

with env.using(search_dirs=[Path("tests/envs")], env_name="test"):
    assert env.get_env("SOME_KEY") == "value"

Development

Virtual Environments

python -m venv venv

Mac/Linux

source venv/bin/activate

Windows

venv/scripts/activate

Install Requirements

pip install poetry
poetry install

Install Optional Requirements

poetry install --extras "decouple"
poetry install --extras "dotenv"
poetry install --extras "decouple dotenv"

Test

pytest
coverage run -m pytest
coverage report
coverage html
mypy --html-report mypy_report .
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --format=html --htmldir="flake8_report/basic" --exclude=venv
flake8 . --count --exit-zero --max-complexity=11 --max-line-length=127 --statistics --format=html --htmldir="flake8_report/complexity" --exclude=venv

BumpVer

With the CLI command bumpver, you can search for and update version strings in your project files. It has a flexible pattern syntax to support many version schemes (SemVer, CalVer or otherwise). Run BumbVer with:

bumpver update --major --dry
bumpver update --major

bumpver update --minor --dry
bumpver update --minor

bumpver update --patch --dry
bumpver update --patch

Build

poetry build

Publish

poetry publish

Automated PyPI Publishing

This project uses GitHub Actions to automatically publish to PyPI when a new version tag is pushed.

Setup (One-time configuration)

  1. Register a Trusted Publisher on PyPI:
    • Go to https://pypi.org/manage/account/publishing/
    • Click "Add a new pending publisher"
    • Fill in the following details:
      • PyPI Project Name: castenv
      • Owner: systemizing-solutions (your GitHub username)
      • Repository name: castenv
      • Workflow name: publish.yml
      • Environment name: pypi
    • Click "Add pending publisher"

How it works

When you use bumpver to update the version:

bumpver update --patch  # or --minor, --major

This will:

  1. Update the version in pyproject.toml, src/castenv/__init__.py, and README.md
  2. Create a git commit with the version bump
  3. Create a git tag (e.g., 4.0.1)
  4. Push the tag to GitHub

GitHub Actions will automatically detect the new tag and:

  1. Build the distribution packages (wheel and source)
  2. Publish to PyPI using the trusted publisher authentication

Security

This approach uses OpenID Connect (OIDC) Trusted Publishers, which is more secure than API tokens because:

  • ✅ No credentials are stored in GitHub secrets
  • ✅ Only this specific workflow can publish
  • ✅ Only from this specific repository
  • ✅ PyPI automatically verifies the request is legitimate

License

MIT

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

castenv-0.3.0.tar.gz (25.3 kB view details)

Uploaded Source

Built Distribution

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

castenv-0.3.0-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: castenv-0.3.0.tar.gz
  • Upload date:
  • Size: 25.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for castenv-0.3.0.tar.gz
Algorithm Hash digest
SHA256 9e51c993d5e34a56af2092695209e92b1fa48a4e65ee7a787f8337365a556c9c
MD5 9e71af0a0d403430bfeb3a12035225ac
BLAKE2b-256 6310026f54fc6fe44105fde9855ef58b17a938f657a46c392743e49b9708ad0c

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on systemizing-solutions/castenv

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

File details

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

File metadata

  • Download URL: castenv-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 21.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for castenv-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 653fe7b515193308106c02ad66fbe189b19e2ca25073db213e72dcb311c3a102
MD5 0d64d967171172f091bb6aac03fde91f
BLAKE2b-256 a176a72fcb853733ba34952893a409aaf34475d6646f17edb671eb48bb5a19fe

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on systemizing-solutions/castenv

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

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