Skip to content

env_proxy.env_config

env_config

EnvConfig allows you to create configuration objects based on env variables declaratively with auto-documenting approach.

EnvField

EnvField(
    alias: str | None = None,
    description: str | None = None,
    default: Any = UNSET,
    env_proxy: EnvProxy | None = None,
    env_prefix: str | None = None,
    strict: bool | None = None,
    allow_set: bool | None = None,
    type_hint: TypeHint | None = None,
    optional: bool | None = None,
    convert_using: Callable[[str], Any] | None = None,
    type_name: str | None = None,
    default_factory: Callable[[], Any] | None = None,
)
Source code in env_proxy/env_config.py
def __init__(
    self,
    alias: str | None = None,
    description: str | None = None,
    default: Any = UNSET,
    env_proxy: EnvProxy | None = None,
    env_prefix: str | None = None,
    strict: bool | None = None,
    allow_set: bool | None = None,
    type_hint: TypeHint | None = None,
    optional: bool | None = None,
    convert_using: Callable[[str], Any] | None = None,
    type_name: str | None = None,
    default_factory: Callable[[], Any] | None = None,
) -> None:
    if default_factory is not None and default is not UNSET:
        raise EnvConfigError("Field() accepts either `default` or `default_factory`, not both.")
    self.alias = alias
    self.description = description
    self._env_proxy = env_proxy
    self._env_prefix = env_prefix
    self._default = default
    self._default_factory = default_factory
    self._field_name: str | None = None
    self._owner: type[EnvConfig] | None = None
    self._value_getter: Callable[[str, Any], Any] | None = None
    self._strict = strict
    self._allow_set = allow_set
    self.optional = optional
    self.type_hint = type_hint
    self._convert_using = convert_using
    self.type_name = type_name
    if convert_using is not None and type_hint is not None:
        warnings.warn(
            f"convert_using overrides type_hint={type_hint!r}; type_hint will be ignored.",
            UserWarning,
            stacklevel=3,
        )
    self._annotation: Any = None

has_default property

has_default: bool

True if the field declares any kind of default (static, factory, or implicit None).

resolved_type_name cached property

resolved_type_name: str

The field's type label — the value shown in exported .env files.

Resolution order:

  1. Explicit type_name= argument, if given.
  2. The field's annotation, when it is a simple type (int, str, an enum class, …).
  3. convert_using.__name__, unless it would be "<lambda>".
  4. The type_hint= value, if given.
  5. "unknown type" as a last resort.

value_getter cached property

value_getter: Callable[[str, Any], Any]

The callable that fetches and types this field's env value.

Called internally on every attribute access; usually you don't need to invoke it yourself. convert_using takes precedence over type_hint, which takes precedence over the annotation.

resolve_default

resolve_default(instance: EnvConfig) -> Any

Return the default value to use for instance.

For factory-defaulted fields, returns the value cached on instance at construction time. Otherwise falls back to the static :attr:default.

Source code in env_proxy/env_config.py
def resolve_default(self, instance: EnvConfig) -> Any:
    """Return the default value to use for ``instance``.

    For factory-defaulted fields, returns the value cached on ``instance``
    at construction time. Otherwise falls back to the static :attr:`default`.
    """
    if self._default_factory is not None:
        return instance._factory_defaults[self.field_name]
    return self.default

EnvConfig

EnvConfig(**overrides: Any)

A base class for your configurations based on environment variables.

Use fields along with Field factory to easily describe your configuration in an self-documenting way.

The constructor accepts keyword arguments to override individual fields on a per-instance basis. Overrides take precedence over the environment, allowing callers to layer env-derived config with values from any other source — a config file, CLI arguments, programmatic wiring, fixtures — without mutating os.environ. Override values are keyed by Python field name (not env-var key), are used as-is (no type conversion), and shadow the environment for reads on this instance only::

cfg = MyConfig(timeout=5, services=["a", "b"])

Unknown override keys raise :class:EnvConfigError (also a :class:ValueError for back-compat). Fields with allow_set=False can still be initialized via override but cannot be reassigned afterwards.

Source code in env_proxy/env_config.py
def __init__(self, **overrides: Any) -> None:
    unknown = overrides.keys() - self._valid_fields
    if unknown:
        raise EnvConfigError(
            f"Unknown override key(s) for {type(self).__name__}: {sorted(unknown)}. "
            f"Valid field names: {sorted(self._valid_fields)}"
        )
    self._overrides: dict[str, Any] = dict(overrides)
    self._frozen: dict[str, Any] | None = None
    self._factory_defaults: dict[str, Any] = {}
    for name, factory in self._factory_fields:
        if name in self._overrides:
            continue
        self._factory_defaults[name] = factory()

is_frozen property

is_frozen: bool

True if :meth:freeze has been called on this instance, else False.

freeze

freeze() -> None

Resolve every field once and lock the instance to the resulting values.

After calling :meth:freeze:

  • Every attribute read returns the cached value (a single dict lookup).
  • Assignment via cfg.field = ... raises :class:TypeError, even for fields declared with allow_set=True. Any such fields are listed in a :class:UserWarning emitted by this call.
  • :attr:is_frozen becomes True.

Calling :meth:freeze again on an already-frozen instance is a no-op and emits a :class:UserWarning.

Eagerly resolves every non-overridden field, so any exception raised during resolution propagates from this call. Unlike :meth:validate, :meth:freeze does not aggregate failures: the first exception is surfaced as-is.

Raises:

Type Description
EnvKeyMissingError

A required field has no env value and no default.

EnvValueError

An env value cannot be converted to the target type.

EnvConfigError

The field is declared incorrectly (e.g. strict-mode annotation problem surfaced at resolution time).

JSONDecodeError

A type_hint="json" field holds invalid JSON. Propagated unchanged — documented deviation, see :mod:env_proxy.exceptions.

Source code in env_proxy/env_config.py
def freeze(self) -> None:
    """Resolve every field once and lock the instance to the resulting values.

    After calling :meth:`freeze`:

    - Every attribute read returns the cached value (a single dict lookup).
    - Assignment via ``cfg.field = ...`` raises :class:`TypeError`, even
      for fields declared with ``allow_set=True``. Any such fields are
      listed in a :class:`UserWarning` emitted by this call.
    - :attr:`is_frozen` becomes ``True``.

    Calling :meth:`freeze` again on an already-frozen instance is a no-op
    and emits a :class:`UserWarning`.

    Eagerly resolves every non-overridden field, so any exception raised
    during resolution propagates from this call. Unlike :meth:`validate`,
    :meth:`freeze` does *not* aggregate failures: the *first* exception
    is surfaced as-is.

    Raises:
        EnvKeyMissingError: A required field has no env value and no default.
        EnvValueError: An env value cannot be converted to the target type.
        EnvConfigError: The field is declared incorrectly (e.g. strict-mode
            annotation problem surfaced at resolution time).
        json.JSONDecodeError: A ``type_hint="json"`` field holds invalid
            JSON. Propagated unchanged — documented deviation, see
            :mod:`env_proxy.exceptions`.
    """
    if self._frozen is not None:
        warnings.warn(
            f"{type(self).__name__} is already frozen; freeze() is a no-op.",
            stacklevel=2,
        )
        return
    snapshot: dict[str, Any] = {}
    mutable: list[str] = []
    for name, field, value in self._iter_resolved_fields():
        snapshot[name] = value
        if field.allow_set:
            mutable.append(name)
    if mutable:
        warnings.warn(
            f"freeze() locked fields with allow_set=True on {type(self).__name__}: "
            f"{sorted(mutable)}. Further assignment will raise TypeError.",
            stacklevel=2,
        )
    self._frozen = snapshot

validate

validate() -> None

Eagerly resolve every field and raise if anything is missing or malformed.

Every :class:EnvProxyError raised during field resolution is captured and aggregated into a single :class:EnvValidationError whose :attr:errors mapping contains the per-field exceptions. This includes :class:EnvKeyMissingError (missing required fields), :class:EnvValueError (failed type conversion or convert_using callback), and :class:EnvConfigError (strict-mode annotation problems surfaced at resolution time). Fields supplied as constructor overrides are already typed Python values, so they are not re-validated.

Returns None on success. Does not mutate the instance — call :meth:freeze afterwards if you also want to lock in the values.

Two classes of exception are not aggregated and bubble out of this call:

  • :class:json.JSONDecodeError from a type_hint="json" field (documented deviation; see :mod:env_proxy.exceptions).
  • Any non-:class:EnvProxyError exception, treated as a library bug so it can be diagnosed rather than silently swallowed.
Source code in env_proxy/env_config.py
def validate(self) -> None:
    """Eagerly resolve every field and raise if anything is missing or malformed.

    Every :class:`EnvProxyError` raised during field resolution is
    captured and aggregated into a single :class:`EnvValidationError`
    whose :attr:`errors` mapping contains the per-field exceptions.
    This includes :class:`EnvKeyMissingError` (missing required
    fields), :class:`EnvValueError` (failed type conversion or
    ``convert_using`` callback), and :class:`EnvConfigError` (strict-mode
    annotation problems surfaced at resolution time). Fields supplied
    as constructor overrides are already typed Python values, so they
    are not re-validated.

    Returns ``None`` on success. Does not mutate the instance — call
    :meth:`freeze` afterwards if you also want to lock in the values.

    Two classes of exception are *not* aggregated and bubble out of
    this call:

    - :class:`json.JSONDecodeError` from a ``type_hint="json"`` field
      (documented deviation; see :mod:`env_proxy.exceptions`).
    - Any non-:class:`EnvProxyError` exception, treated as a library
      bug so it can be diagnosed rather than silently swallowed.
    """
    errors: dict[str, EnvProxyError] = {}
    cls = type(self)
    for name in self._valid_fields:
        if name in self._overrides:
            continue
        try:
            field: EnvField = getattr(cls, name)
            field.value_getter(field.key_name, field.resolve_default(self))
        except EnvProxyError as exc:
            errors[name] = exc
    if errors:
        raise EnvValidationError(type(self).__name__, errors)