EnvBind
EnvBind gives you typed environment parsing with explicit descriptors.
Define a class, run it, and use attributes as normal values.
Acknowledgement
EnvBind follows a class-based configuration style inspired by Pydantic.
The design goal is simpler: parse and validate environment values at load time with a small, focused API.
Install
uv sync --group dev
The package requires Python 3.10 or newer.
Quick start
from envbind import B64DecodedStringEnv, ParameterSource, StringEnv
class DatabaseSettings(ParameterSource):
user: str = StringEnv(envar="USER")
password: str = StringEnv(envar="PWD")
certificate: str = B64DecodedStringEnv(envar="CERTIFICATE")
export USER=admin
export PWD=secret
export CERTIFICATE=c29tZSB4NTA5IGNlcnQgaGVyZSBlbmNvZGVkIGluIGJhc2U2NA==
params = DatabaseSettings()
print(params.user)
print(params.password)
print(params.certificate)
StringEnv validates non-empty values.
optional=True plus a default value loads the default when the variable is absent.
class DatabaseSettings(ParameterSource):
user: str = StringEnv(envar="USER", optional=True, default="admin")
By default, empty strings are handled as missing values.
Use allow_empty=True to parse an empty string as a real value.
class TextSettings(ParameterSource):
label: str = StringEnv(envar="LABEL", allow_empty=True)
Design goals from a code perspective
EnvBind addresses common design debt in environment-driven code.
Many projects read environment variables in each service class. This spreads parsing logic across many files. It also hides failure points until runtime.
This library keeps parsing and validation in one place. Each class defines one schema and one responsibility. That avoids duplicate reads and repeated conversion code.
The next issue is test setup. Business classes often hard-code real clients during construction. That creates fragile tests and slow test runs.
ParameterSource makes parameter objects explicit.
The object can be passed to business code as a dependency.
The object can then be swapped for a mock without changing call sites.
The last issue is design drift. When environment details and business logic mix, changes become risky. With parameter objects, both logic and external settings stay separate. That supports the Open-Closed principle and safe subclassing in tests.
Exception handling
EnvBind raises two runtime exceptions for parsing contracts.
MissingEnvironmentVariableErrorwhen a required variable is not present.InvalidEnvironmentValueErrorwhen the value cannot be parsed.
EnvParseError is the base exception for both types.
from envbind import (
ParameterSource,
EnvParseError,
InvalidEnvironmentValueError,
MissingEnvironmentVariableError,
IntEnv,
StringEnv,
)
class Settings(ParameterSource):
user: str = StringEnv(envar="USER")
port: int = IntEnv(envar="PORT")
try:
settings = Settings()
except MissingEnvironmentVariableError as error:
raise RuntimeError(f"Missing configuration: {error}") from error
except InvalidEnvironmentValueError as error:
raise RuntimeError(f"Invalid configuration format: {error}") from error
except EnvParseError as error:
raise RuntimeError(f"General parser error: {error}") from error
When optional=True, EnvField does not raise on missing values.
If a default exists, the default returns first.
If no default exists, the resolved value becomes None.
class OptionalSettings(ParameterSource):
user: str | None = StringEnv(
envar="USER",
optional=True,
)
Custom parsing with your own EnvField
ParameterSource ships with typed fields for common data and custom logic needs.
Built in:
BooleanEnvFloatEnvListEnvJSONEnvEnumEnv
from enum import Enum
from envbind import BooleanEnv, ParameterSource, EnumEnv, FloatEnv, JSONEnv, ListEnv
class DeploymentMode(Enum):
BLUE = "blue"
GREEN = "green"
class ServiceSettings(ParameterSource):
enabled: bool = BooleanEnv(envar="SERVICE_ENABLED")
timeout: float = FloatEnv(envar="SERVICE_TIMEOUT_SECONDS")
hosts: list[str] = ListEnv(envar="SERVICE_HOSTS", delimiter=",")
profile: dict[str, object] = JSONEnv(envar="SERVICE_PROFILE")
mode: DeploymentMode = EnumEnv(envar="SERVICE_MODE", enum_type=DeploymentMode)
ListEnv can parse each item into another type.
class NetworkSettings(ParameterSource):
ports: list[int] = ListEnv(
envar="SERVICE_PORTS",
element_type=int,
sensitive=False,
)
Validation helpers
Attach a validator to any field.
The validator receives the parsed value and raises ValueError on invalid input.
from envbind import IntEnv, ParameterSource, StringEnv, validators
class ApiSettings(ParameterSource):
port: int = IntEnv(
envar="PORT",
validator=validators.in_range(1, 65535),
sensitive=False,
)
environment: str = StringEnv(
envar="ENVIRONMENT",
validator=validators.one_of("dev", "staging", "prod"),
sensitive=False,
)
Built-in validators:
in_range(min_val, max_val)min_value(min_val)max_value(max_val)one_of(*allowed)min_length(length)max_length(length)matches_pattern(pattern)is_url()is_email()all_of(*validators)
Sensitive values
Fields are sensitive by default.
Sensitive fields are hidden from repr(), observer logs, and as_dict().
class SecretSettings(ParameterSource):
token: str = StringEnv(envar="API_TOKEN")
host: str = StringEnv(envar="API_HOST", sensitive=False)
settings = SecretSettings()
print(settings)
print(settings.as_dict())
print(settings.as_dict(include_sensitive=True))
Use sensitive=False for values that are safe to show, such as ports,
hostnames, modes, and feature flags.
Observability
Register an observer to receive configuration loading events.
import logging
from envbind import LoggingObserver, ParameterSource
logging.basicConfig(level=logging.INFO)
ParameterSource.set_observer(LoggingObserver())
Use StructuredObserver for log processors that read structured fields.
For custom telemetry, implement ConfigurationObserver.
You can still create your own field classes for special rules.
from envbind import EnvField, ParameterSource, InvalidEnvironmentValueError
class SecretValue:
"""Container that hides secret text."""
def __init__(self, value: str) -> None:
"""Keep raw value stored privately."""
self._value = value
def reveal(self) -> str:
"""Expose the real value only when called."""
return self._value
def __str__(self) -> str:
"""Hide value in string output."""
return "<secret>"
def __repr__(self) -> str:
"""Hide value in object repr."""
return "<secret>"
class SecretEnv(EnvField):
"""Read a secret and keep it masked in logs."""
def _coerce(self, raw: str) -> SecretValue:
"""Reject empty secrets and return a safe wrapper."""
if raw == "":
raise InvalidEnvironmentValueError("SECRET value must not be empty")
return SecretValue(raw)
class SecretSettings(ParameterSource):
api_key: SecretValue = SecretEnv(envar="API_KEY")
SecretValue exposes the token only through reveal().
That avoids accidental text logging.
Dependency injection via parameter objects
Use subclassing for dependency injection in tests while keeping business code stable.
from typing import Any, Protocol
from pymongo import MongoClient
from envbind import ParameterSource, B64DecodedStringEnv, StringEnv, IntEnv
class MongoConnector(Protocol):
@property
def db(self) -> Any:
...
class MongoConnectionSettings(ParameterSource):
user: str = StringEnv(envar="USER")
password: str = StringEnv(envar="PWD")
certificate: str = B64DecodedStringEnv(envar="CERTIFICATE")
server: str = StringEnv(envar="SERVER", optional=True, default="localhost")
port: int = IntEnv(envar="PORT", optional=True, default=27017)
database: str = StringEnv(envar="DB", optional=True, default="test_db")
@property
def connection_str(self) -> str:
return f"mongodb://{self.user}:{self.password}@{self.server}:{self.port}/{self.database}"
@property
def db(self) -> Any:
client = MongoClient(self.connection_str)
return client[self.database]
class UserDocumentRepository:
def __init__(self, connection: MongoConnector):
self._connection = connection
def add(self, user: dict[str, object]) -> None:
self._connection.db["users"].insert_one(user)
Swap MongoConnectionSettings with a test subclass to inject a mock database.
from typing import Any
import mongomock
class MockMongoConnectionSettings(MongoConnectionSettings):
def __init__(self) -> None:
super().__init__()
self._mock_client = mongomock.MongoClient()
@property
def db(self) -> Any:
return self._mock_client[self.database]
from typing import Any
from unittest.mock import patch
import pytest
def _load_test_user() -> dict[str, object] | None:
with patch.dict(
"os.environ",
{
"USER": "admin",
"PWD": "secret",
"CERTIFICATE": "c29tZSB4NTA5IGNlcnQgaGVyZSBlbmNvZGVkIGluIGJhc2U2NA==",
"SERVER": "localhost",
"PORT": "27017",
"DB": "users_db",
},
):
connection = MockMongoConnectionSettings()
repository = UserDocumentRepository(connection=connection)
repository.add({"email": "alice@example.com"})
return repository._connection.db["users"].find_one({"email": "alice@example.com"})
def test_user_document_repository_add_inserts_user_record() -> None:
record = _load_test_user()
assert record is not None
def test_user_document_repository_add_records_email_field() -> None:
record = _load_test_user()
assert record["email"] == "alice@example.com"
mongomock is optional and only needed for this test path.
Testing ParameterSource subclasses with pytest
The package is easiest to test with in-memory environment values.
monkeypatch makes each test fully isolated.
from __future__ import annotations
import pytest
from envbind import IntEnv, MissingEnvironmentVariableError, ParameterSource, StringEnv
class DatabaseParameters(ParameterSource):
user: str = StringEnv(envar="TEST_USER")
port: int = IntEnv(envar="TEST_PORT", optional=True, default=5432)
def test_database_parameters_reads_values(monkeypatch: pytest.MonkeyPatch) -> None:
"""Build values from environment settings."""
monkeypatch.setenv("TEST_USER", "admin")
monkeypatch.setenv("TEST_PORT", "27017")
params = DatabaseParameters()
assert params.user == "admin"
def test_database_parameters_uses_default(monkeypatch: pytest.MonkeyPatch) -> None:
"""Use defaults for optional fields."""
monkeypatch.delenv("TEST_PORT", raising=False)
monkeypatch.setenv("TEST_USER", "admin")
params = DatabaseParameters()
assert params.port == 5432
def test_database_parameters_raises_on_missing_user(monkeypatch: pytest.MonkeyPatch) -> None:
"""Raise an explicit parse error for missing required fields."""
monkeypatch.delenv("TEST_USER", raising=False)
with pytest.raises(MissingEnvironmentVariableError):
DatabaseParameters()
If your repository uses dependency injection, pass the parsed object to your business code and test it with fake data adapters.
Development
This repo uses uv for dependencies.
uv sync --group dev
pre-commit install --hook-type pre-commit --hook-type pre-push
Run format, lint, and type checks:
pre-commit run --all-files
Pre-push hook runs tests with coverage using:
pytest --cov=envbind --cov-branch --cov-fail-under=100
This project includes pre-commit and pre-push hooks.
Commits run Ruff and Mypy.
Pushes run unit tests with a 100% coverage gate.
Testing
uv run pytest
All tests live under tests/envbind.
Each test method uses one assertion to keep scope tight.
License
EnvBind is released under the MIT License.
Metadata
Release files for envbind 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| envbind-1.1.0.tar.gz | 18.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| envbind-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 48.1 kB
Release files / envbind-1.1.0.tar.gz
| Download URL | envbind-1.1.0.tar.gz |
|---|---|
| Size | 18.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
639cc71bdf75e402d7bac6757e427ebab1d2afe185b0bef86977d713a343575c
|
|
BLAKE2b-256 checksum How to use checksums |
bbebed2a309cadb2e15ea377ae8d446a0ac2222b20f595bf6cc099743f02ca92
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Apr 19, 2026.
Transparency logRelease files / envbind-1.1.0-py3-none-any.whl
| Download URL | envbind-1.1.0-py3-none-any.whl |
|---|---|
| Size | 29.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c1d502397559566b896c03025c0cffac55b5b92ab9233b7e240b3be74c167f4e
|
|
BLAKE2b-256 checksum How to use checksums |
db027c0dcf126390d7bdf1780a9777ea65c5b7f49e9600e73bd74abf60035aab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Apr 19, 2026.
Transparency log