Skip to content

Basic Usage

Variable Expansion

envresolve supports variable expansion using ${VAR} and $VAR syntax.

Simple Variable Expansion

from envresolve import expand_variables

env = {"VAULT_NAME": "my-vault"}
result = expand_variables("${VAULT_NAME}", env)

print(result)  # Output: my-vault
from envresolve import expand_variables

env = {"VAULT_NAME": "my-vault"}
result = expand_variables("$VAULT_NAME", env)

print(result)  # Output: my-vault

Multiple Variables

You can reference multiple variables in a single string:

from envresolve import expand_variables

env = {
    "VAULT_NAME": "my-vault",
    "SECRET_NAME": "db-password"
}
result = expand_variables("akv://${VAULT_NAME}/${SECRET_NAME}", env)

print(result)  # Output: akv://my-vault/db-password

Nested Variable Expansion

Variables can reference other variables:

from envresolve import expand_variables

env = {
    "ENVIRONMENT": "prod",
    "VAULT_NAME": "${ENVIRONMENT}-vault",
    "SECRET_URI": "akv://${VAULT_NAME}/api-key"
}
result = expand_variables(env["SECRET_URI"], env)

print(result)  # Output: akv://prod-vault/api-key

Using Environment Variables

With os.environ

Use EnvExpander to expand variables from the current environment:

import os
from envresolve import EnvExpander

# Set environment variable
os.environ["VAULT_NAME"] = "production-vault"

expander = EnvExpander()
result = expander.expand("akv://${VAULT_NAME}/secret")

print(result)  # Output: akv://production-vault/secret

Snapshot Behavior

EnvExpander takes a snapshot of os.environ at initialization time. Changes to environment variables after initialization won't be reflected.

With .env Files

Use DotEnvExpander to expand variables from a .env file:

from envresolve import DotEnvExpander

# Contents of .env:
# VAULT_NAME=my-company-vault
# DB_PASSWORD=akv://${VAULT_NAME}/db-password
# API_KEY=akv://${VAULT_NAME}/api-key

expander = DotEnvExpander(".env")
db_password_uri = expander.expand("${DB_PASSWORD}")
api_key_uri = expander.expand("${API_KEY}")

print(db_password_uri)  # Output: akv://my-company-vault/db-password
print(api_key_uri)      # Output: akv://my-company-vault/api-key

Secret Resolution

envresolve can resolve secrets referenced with akv:// URIs. Provider registration is explicit so you only pay the dependency cost when you opt in.

1. Install the Azure extra (once per environment)

pip install envresolve[azure]

2. Register the provider

import envresolve

envresolve.register_azure_kv_provider()

register_azure_kv_provider() is idempotent—you can call it during application startup without worrying about duplicate work.

3. Resolve a secret URI

import envresolve

envresolve.register_azure_kv_provider()

# Plain strings are returned unchanged (idempotent behaviour)
print(envresolve.resolve_secret("db-password"))  # db-password

# Secret URIs fetch values from Azure Key Vault
password = envresolve.resolve_secret("akv://corp-vault/db-password")
print(password)

Custom Provider Configuration

For advanced scenarios like testing or custom authentication, you can inject a custom provider instance:

import envresolve
from envresolve.providers.azure_kv import AzureKVProvider
from azure.identity import ManagedIdentityCredential

# Create custom provider with specific credential
custom_provider = AzureKVProvider(
    credential=ManagedIdentityCredential(client_id="your-client-id")
)

# Register the custom provider
envresolve.register_azure_kv_provider(provider=custom_provider)

# Now use envresolve as normal
secret = envresolve.resolve_secret("akv://vault/secret")

This is particularly useful for:

  • Testing: Inject mock providers without patching internal implementation details
  • Custom authentication: Use specific Azure credentials (service principal, managed identity with client ID, etc.)
  • Provider configuration: Pre-configure providers with custom settings before registration

Iterative resolution

resolve_secret() keeps resolving until the returned value is stable. This lets you chain indirections or mix URI results with variable expansion:

import os
import envresolve

envresolve.register_azure_kv_provider()

os.environ["ENVIRONMENT"] = "prod"

# akv://config/service → "akv://vault-${ENVIRONMENT}/service"
secret = envresolve.resolve_secret("akv://config/service")
print(secret)  # Resolved value from akv://vault-prod/service

Loading and exporting from .env

import os
import envresolve

envresolve.register_azure_kv_provider()

# .env may contain plain values, variable references, and akv:// URIs
# By default, searches for .env in current directory and exports to os.environ
resolved = envresolve.load_env()

print(resolved["DB_PASSWORD"])
print(os.environ["DB_PASSWORD"])  # Exported unless override=False and already set

Use export=False when you only need the resolved dictionary, or set override=True if you want to intentionally replace existing os.environ values.

Complete python-dotenv compatibility

For exact python-dotenv search behavior (searching from calling script location instead of current working directory), use this pattern:

from dotenv import load_dotenv
import envresolve

# Use python-dotenv's search behavior
load_dotenv()

# Resolve secrets in os.environ
envresolve.register_azure_kv_provider()
envresolve.resolve_os_environ()

This preserves python-dotenv's exact search semantics while adding secret resolution capabilities.

Resolving Existing Environment Variables

Use resolve_os_environ() to resolve secret URIs that are already set in os.environ. This is useful when environment variables are passed from parent shells or container orchestrators:

import os
import envresolve

envresolve.register_azure_kv_provider()

# Environment variables set by parent process or container
os.environ["API_KEY"] = "akv://prod-vault/api-key"
os.environ["DB_PASSWORD"] = "akv://prod-vault/db-password"

# Resolve all environment variables containing secret URIs
resolved = envresolve.resolve_os_environ()

print(resolved["API_KEY"])      # Resolved secret value
print(os.environ["API_KEY"])    # os.environ is updated by default

Filtering by Specific Keys

Resolve only specific environment variables:

import os
import envresolve

envresolve.register_azure_kv_provider()

os.environ["API_KEY"] = "akv://prod-vault/api-key"
os.environ["DB_PASSWORD"] = "akv://prod-vault/db-password"
os.environ["PLAIN_CONFIG"] = "some-value"

# Resolve only API_KEY and DB_PASSWORD
resolved = envresolve.resolve_os_environ(keys=["API_KEY", "DB_PASSWORD"])

# PLAIN_CONFIG is not processed
assert "PLAIN_CONFIG" not in resolved

Filtering by Prefix

Resolve variables with a specific prefix and strip the prefix from output:

import os
import envresolve

envresolve.register_azure_kv_provider()

# Different environments using prefixes
os.environ["DEV_API_KEY"] = "akv://dev-vault/api-key"
os.environ["DEV_DB_URL"] = "akv://dev-vault/db-url"
os.environ["PROD_API_KEY"] = "akv://prod-vault/api-key"
os.environ["PROD_DB_URL"] = "akv://prod-vault/db-url"

# Resolve only DEV_ variables and strip the prefix
resolved = envresolve.resolve_os_environ(prefix="DEV_")

# Results have prefix stripped
print(resolved["API_KEY"])      # Resolved from DEV_API_KEY
print(resolved["DB_URL"])       # Resolved from DEV_DB_URL

# os.environ is updated with stripped keys
print(os.environ["API_KEY"])    # Resolved value
assert "DEV_API_KEY" not in os.environ  # Old key removed

Prefix Stripping Behavior

When using prefix, the resolved values are stored in os.environ with the prefix stripped, and the original prefixed keys are removed.

Without Updating os.environ

Use overwrite=False to get resolved values without modifying os.environ:

import os
import envresolve

envresolve.register_azure_kv_provider()

os.environ["API_KEY"] = "akv://prod-vault/api-key"

# Get resolved values without updating os.environ
resolved = envresolve.resolve_os_environ(overwrite=False)

print(resolved["API_KEY"])              # Resolved secret value
print(os.environ["API_KEY"])            # Still the original URI
assert os.environ["API_KEY"] == "akv://prod-vault/api-key"

Continuing on Errors

Use stop_on_expansion_error=False and stop_on_resolution_error=False to control error handling granularly:

import os
import envresolve

envresolve.register_azure_kv_provider()

os.environ["GOOD_KEY"] = "akv://prod-vault/valid-secret"
os.environ["BAD_KEY"] = "akv://prod-vault/missing-secret"  # Doesn't exist
os.environ["MISSING_VAR"] = "${UNDEFINED}"
os.environ["PLAIN"] = "plain-value"

# Skip variables with missing references, but still raise on secret resolution errors
resolved = envresolve.resolve_os_environ(stop_on_expansion_error=False)

# Skip variables with secret resolution errors, but still raise on undefined variables
resolved = envresolve.resolve_os_environ(stop_on_resolution_error=False)

# Skip both types of errors
resolved = envresolve.resolve_os_environ(
    stop_on_expansion_error=False,
    stop_on_resolution_error=False
)

# Successfully resolved variables are in the result
print(resolved["GOOD_KEY"])    # Resolved value
print(resolved["PLAIN"])       # plain-value
assert "BAD_KEY" not in resolved  # Skipped due to secret resolution error
assert "MISSING_VAR" not in resolved  # Skipped due to expansion error

CircularReferenceError is always raised

CircularReferenceError is always raised regardless of these flags, as it indicates a configuration error that cannot be resolved.

Ignoring Specific Variables

Use ignore_keys to skip variable expansion and secret resolution for specific keys. This is useful when certain variables should be preserved as-is:

import os
import envresolve

os.environ["PS1"] = "${USER}@${HOST}$ "  # Shell prompt template
os.environ["API_KEY"] = "akv://vault/api-key"

# Skip expansion for PS1
resolved = envresolve.resolve_os_environ(ignore_keys=["PS1"])

print(resolved["PS1"])        # Output: ${USER}@${HOST}$  (unchanged)
print(resolved["API_KEY"])    # Resolved secret value

Ignored variables are included in the result as-is without any processing.

When to use ignore_keys:

  • Skip specific variables while maintaining strict error checking for others
  • Preserve variables containing literal $ characters that shouldn't be expanded
  • Temporarily exclude problematic variables during debugging

Difference from stop_on_expansion_error=False:

  • ignore_keys: Selectively excludes specific variables by name
  • stop_on_expansion_error=False: Suppresses all expansion errors globally

Use ignore_keys when you know exactly which variables to skip, and stop_on_expansion_error=False when you want lenient error handling across all variables.

Ignoring Variables by Pattern

Use ignore_patterns to skip variables using glob-style pattern matching. This is useful when you want to exclude groups of related variables:

import os
import envresolve

# Shell prompt variables
os.environ["PS1"] = "${USER}@${HOST}$ "
os.environ["PS2"] = "> "
os.environ["PS4"] = "+ "
os.environ["PROMPT"] = "${PWD}$ "
os.environ["API_KEY"] = "akv://vault/api-key"

# Ignore all prompt-related variables using patterns
resolved = envresolve.resolve_os_environ(ignore_patterns=["PS*", "PROMPT*"])

print(resolved["PS1"])        # Output: ${USER}@${HOST}$  (unchanged)
print(resolved["PS2"])        # Output: >  (unchanged)
print(resolved["PROMPT"])     # Output: ${PWD}$  (unchanged)
print(resolved["API_KEY"])    # Resolved secret value

Supported wildcards:

  • * - matches any characters (e.g., PS* matches PS1, PS2, PROMPT)
  • ? - matches single character (e.g., PS? matches PS1, but not PS10)
  • [seq] - matches any character in seq (e.g., PS[12] matches PS1, PS2)

Common use cases:

# System shell variables
ignore_patterns=["PS*", "PROMPT*", "BASH_*"]

# Temporary variables
ignore_patterns=["TEMP_*", "TMP_*"]

# Debug flags
ignore_patterns=["DEBUG_*", "TRACE_*"]

Best practices:

Avoid overly broad patterns

Be specific with your patterns to avoid accidentally excluding variables you need:

# ❌ Too broad - excludes everything starting with 'A'
ignore_patterns=["A*"]

# ✅ Specific - only excludes AWS temporary credentials
ignore_patterns=["AWS_SESSION_*"]

Combining exact match and patterns

Use both ignore_keys and ignore_patterns for maximum flexibility:

resolved = envresolve.resolve_os_environ(
    ignore_keys=["SPECIFIC_VAR"],        # Exact match
    ignore_patterns=["TEMP_*", "DEBUG_*"]  # Pattern match
)

Execution order:

  1. Check ignore_keys (exact match - fast path)
  2. If not matched, check ignore_patterns (pattern match)
  3. If neither matched, perform resolution

When to use ignore_patterns vs ignore_keys:

  • ignore_patterns: Exclude groups of related variables (e.g., all PS* shell prompts)
  • ignore_keys: Exclude specific individual variables by exact name

Error Handling

When working with external services, it's important to handle potential errors like missing dependencies, incorrect configuration, or network issues.

Provider and Resolution Errors

Here is a robust example of how to handle errors during provider registration and secret resolution:

import envresolve
from envresolve.exceptions import ProviderRegistrationError, SecretResolutionError

try:
    # This might fail if 'envresolve[azure]' is not installed
    envresolve.register_azure_kv_provider()

    # This might fail due to permissions, network issues, or if the secret doesn't exist
    secret_value = envresolve.resolve_secret("akv://corp-vault/db-password")
    print(secret_value)

except ProviderRegistrationError as e:
    print(f"Provider setup failed: {e}")
    # Example: Provider setup failed: Azure Key Vault provider requires: azure-identity, azure-keyvault-secrets. Install with: pip install envresolve[azure]

except SecretResolutionError as e:
    print(f"Failed to fetch secret: {e}")

This pattern ensures that your application can gracefully handle both setup-time (missing dependencies) and run-time (secret access) errors.

Environment Variable Resolution Errors

When load_env() or resolve_os_environ() fails, EnvironmentVariableResolutionError indicates which variable was being processed:

import envresolve

try:
    envresolve.load_env(dotenv_path=".env", export=False)
except envresolve.EnvironmentVariableResolutionError as e:
    print(f"Failed variable: {e.context_key}")
    print(f"Cause: {e.original_error}")

    # Access original error details
    if isinstance(e.original_error, envresolve.VariableNotFoundError):
        print(f"Missing: {e.original_error.variable_name}")
    elif isinstance(e.original_error, envresolve.SecretResolutionError):
        print(f"Failed URI: {e.original_error.uri}")

The exception has two attributes: context_key (variable name) and original_error (underlying exception). CircularReferenceError is not wrapped

Circular Reference Detection

envresolve automatically detects circular references and raises a clear error:

from envresolve import expand_variables
from envresolve.exceptions import CircularReferenceError

env = {
    "A": "${B}",
    "B": "${A}"
}

try:
    result = expand_variables(env["A"], env)
except CircularReferenceError as e:
    print(f"Error: {e}")
    # Error: Circular reference detected: B -> A -> B

    # Inspect the exact cycle if you need more detail
    print(e.chain)  # ['B', 'A', 'B']

Missing Variable Error

If a referenced variable doesn't exist, VariableNotFoundError is raised:

from envresolve import expand_variables
from envresolve.exceptions import VariableNotFoundError

env = {"A": "value"}

try:
    result = expand_variables("${MISSING}", env)
except VariableNotFoundError as e:
    print(f"Error: {e}")
    # Error: Variable 'MISSING' not found

Mutually Exclusive Arguments

Some API functions have mutually exclusive parameters. For example, resolve_os_environ() cannot accept both keys and prefix parameters:

import envresolve
from envresolve.exceptions import MutuallyExclusiveArgumentsError

envresolve.register_azure_kv_provider()

try:
    # This will raise an error - cannot specify both
    envresolve.resolve_os_environ(
        keys=["API_KEY"],
        prefix="DEV_"
    )
except MutuallyExclusiveArgumentsError as e:
    print(f"Error: {e}")
    # Error: Arguments 'keys' and 'prefix' are mutually exclusive.
    #        Specify either 'keys' or 'prefix', but not both.

    # Access argument names programmatically
    print(f"Conflicting arguments: {e.arg1} and {e.arg2}")
    # Conflicting arguments: keys and prefix

TypeError Compatibility

MutuallyExclusiveArgumentsError also inherits from TypeError, so you can catch it with standard exception handling:

try:
    envresolve.resolve_os_environ(keys=["API_KEY"], prefix="DEV_")
except TypeError as e:
    print(f"Invalid argument combination: {e}")

Advanced Use Cases

Building Secret URIs Dynamically

from envresolve import expand_variables

# Define vault and environment once
env = {
    "ENVIRONMENT": "production",
    "VAULT": "${ENVIRONMENT}-keyvault",

    # Define all secrets using the vault
    "DB_HOST_URI": "akv://${VAULT}/db-host",
    "DB_USER_URI": "akv://${VAULT}/db-user",
    "DB_PASS_URI": "akv://${VAULT}/db-password",
    "API_KEY_URI": "akv://${VAULT}/api-key",
}

# Expand each URI
for key in ["DB_HOST_URI", "DB_USER_URI", "DB_PASS_URI", "API_KEY_URI"]:
    expanded = expand_variables(env[key], env)
    print(f"{key}: {expanded}")

# Output:
# DB_HOST_URI: akv://production-keyvault/db-host
# DB_USER_URI: akv://production-keyvault/db-user
# DB_PASS_URI: akv://production-keyvault/db-password
# API_KEY_URI: akv://production-keyvault/api-key

Plain Text Pass-Through

Text without variable references is returned unchanged:

from envresolve import expand_variables

result = expand_variables("plain text with $100 price", {"VAR": "value"})
print(result)  # Output: plain text with $100 price

Note: A lone $ followed by non-variable characters (like digits) is preserved.

Secret Resolution Errors

Azure-specific failures (missing vaults, permission issues, network errors) raise SecretResolutionError. The exception carries the failing URI, making it easy to log or surface to users:

import envresolve
from envresolve.exceptions import SecretResolutionError

envresolve.register_azure_kv_provider()

try:
    envresolve.resolve_secret("akv://missing-vault/api-key")
except SecretResolutionError as exc:
    print(exc)              # Human-readable message
    print(exc.uri)          # akv://missing-vault/api-key
    print(exc.original_error)  # Underlying Azure exception (if available)