Dynamic variables for static namespaces
Project description
standby-variables
Dynamic variables for static namespaces.
About
standby-variables is a tiny library for declaring dynamic values (like environment variables) in static, typed namespaces (like Python classes).
- Treat environment configuration as first-class, typed values.
- Compose behavior with a small and expressive API.
- Keep your code statically analyzable: every variable has a concrete type, and mypy understands it.
You get:
- Plain attributes on classes that evaluate lazily.
- A consistent way to make variables optional or provide defaults.
- Validation and error messages that carry useful context.
- Building blocks to extend beyond environment variables.
Rationale
Most applications mix a static structure (modules, classes) with dynamic sources (env vars, files, CLI args).
It’s tempting to sprinkle os.environ.get(...) everywhere - but that dilutes typing, validation, and clarity.
With standby-variables your dynamic values become explicit and composable:
- Declare what a variable is, how it’s parsed, and what to do if it’s missing.
- Chain behavior using two operators:
>>to apply "hints" (Default, Required, Validated)|to provide a backup variable
- Keep strong typing across your configuration surface.
- Use dataclass-like descriptors that work as class attributes and cache nothing implicitly.
The result is readable configuration code that feels like constants, but evaluates at runtime.
Basic usage and syntax
- Every dynamic value is a
Variable[T]for some typeT. - You can "extract" the runtime value by either:
- Calling the variable:
var()(may returnNoneif defined as not required), - Or forcing the value:
var.value()(never returnsNone; raises if unavailable).
- Calling the variable:
- String representations include context, and exceptions propagate that context upward, which makes debugging easier.
Hints
Default(value)returns the default when the source is missing.Required(True)(default) means the variable must be present.Required(False)means "optional":var()can returnNone.Validated(predicate, raises=True)ensures values pass a check.Validated(predicate, raises=False)can "nullify" invalid values (returningNonevia.__call__) which you can then back up with|or keep asNoneif optional.
Example:
import os
from standby import Const
from standby.hint import Default, Required, Validated
from standby import env
# simulate environment
os.environ.setdefault("APP_PORT", "8080")
def parse_bool(s: str) -> bool:
return s.strip() in {"1", "true", "yes", "on", "True"}
class Settings:
# A required int, with a default when missing
PORT: int =~ (env.Var("APP_PORT", int) >> Default(8000))
# Optional bool: returns None if missing
DEBUG: bool | None =~ (env.Var("APP_DEBUG", parse_bool) >> Required(False))
# A purely static value with validation
TIMEOUT: int =~ (Const(10) >> Validated(lambda v: v > 0))
print(Settings.PORT) # 8080 (from env), defaults to 8000 if unset
print(Settings.DEBUG) # None if APP_DEBUG is missing; True/False otherwise
print(Settings.TIMEOUT) # 10
Backups with |:
from standby import env, Const
class Settings:
class API:
# Prefer ENV var; if missing, use a constant fallback
URL: str =~ (env.Var("API_URL", str) | Const("https://api.example.com"))
print(Settings.API.URL) # Returns ENV value if present, otherwise fallback
Forcing with .value():
from standby import env
from standby.exc import VariableNotSet
try:
must_have_port = env.Var[int]("MUST_HAVE_PORT", int).value()
except VariableNotSet as e:
# Rich context in e.args for easier debugging
print("Missing env:", e.args)
Type safety
Every variable is parameterized by its type, and mypy can verify usage end-to-end.
You might have noticed a weird operator =~ used in example of Settings class.
In fact, it is two distinct operators: = and ~ joined together due to subjective preference.
It is possible to have ~ attached to Variable definition like:
URL: str = ~(...) # this is why expression is inside parenthesis
The role of this operator is to instruct type checker that nothing wrong happens when we define Variable[T] as its
wrapped type T. When you access a variable via parent class/instance, Python invokes __get__() descriptor
method which is implemented for all basic primitives in standby. So, reading the value T "dynamically" happens just
by accessing its container Variable[T] which is initialized together with its parent class. You can think about it as
a special case of "class property".
from standby import Const
from standby.hint import Default, Required, Validated
from standby import env
def parse_bool(s: str) -> bool:
return s.strip() in {"1", "true", "yes", "on", "True"}
class Settings:
# Expression has real type Variable[int] but defined inside a class as just int
PORT: int =~ (env.Var("APP_PORT", int) >> Default(8000))
# Expression has real type Variable[bool] but is set to "bool | None" because it's hinted with Required(false)
# Currently, unary operator casting to "T | None" is not implemented
# Type checker (mypy) allows result of cast(T, ...) to be assigned a "T | None" type
DEBUG: bool | None =~ (env.Var("APP_DEBUG", bool) >> Required(False))
Environment variables
The standby.env module provides handy tools to read from os.environ.
env.Var[T](name, parser)readsos.environ[name]and parses it into typeT.env.SeparatedList[T]takes a sourceVariable[str], splits by a givensplit_sep(,by default), and parses each element.env.Ref[T]takes a sourceVariable[str]and uses its value to find another environment variable with that name.
import base64
from standby import Const
from standby.hint import Default, Required
from standby import env
# Required int with a default
POOL_SIZE = env.Var[int]("DB_POOL_SIZE", int) >> Default(5)
# Optional boolean
USE_CACHE = env.Var[bool]("USE_CACHE", lambda s: s == "1") >> Required(False)
# ALLOWED_HOSTS="example.com, api.example.com, localhost"
ALLOWED_HOSTS = env.SeparatedList[str](
src=env.Var("ALLOWED_HOSTS", str), # read raw string from env
split_sep=",",
parser=lambda s: s.strip(), # trim each piece
)
# Default to ["localhost"] if the variable is missing
ALLOWED_HOSTS_WITH_DEFAULT = ALLOWED_HOSTS >> Default(["localhost"])
# If ALLOWED_HOSTS is missing, and you want "optional list" semantics:
OPTIONAL_HOSTS = ALLOWED_HOSTS >> Required(False) # .__call__ returns None
# SECRET_ENV_VAR="<AUTO_GENERATED_VARIABLE_WITH_SECRET_VALUE_IN_BASE64>"
SECRET = env.Ref(
src=env.Var("SECRET_ENV_VAR", str), # taking the secret var name here
parser=base64.b64decode, # decoding it to bytes
)
decoded_secret: bytes = SECRET()
Link and List customization
env.Ref and env.SeparatedList are built from standby primitives: Link and List.
Links let one variable refer to another variable’s name.
This is handy for indirection (think: "the name of the variable that contains the key").
Link[T, S]: a generic link-variable where:srcyields someS;linkermaps thatSto anotherVariable[T].
List[T, P, S]: a generic list-variable where:srcyields someS;Sis split in parts ofPby asplitterfunction given as keyword arg;- Each
Ppart is parsed toTby aparserfunction given as keyword arg; Titems are used to create a newVariable[list[T]].
Custom link can be used like this:
import os
from standby import Link, env, exc, hint, Variable
# Suppose "WORKERS_VAR" contains the name of another env var that holds an int.
# e.g. WORKERS_VAR="MAX_WORKERS", and MAX_WORKERS="16"
ENV_INT = env.Var.factory(parser=int)
MAX_WORKERS = Link(
src=env.Var("WORKERS_VAR", str), # the name of the target var
linker=ENV_INT, # how to create Var[int] from the name
)
try:
print(MAX_WORKERS())
except exc.VariableNotSet:
# Missing source raises exception
raise
# Now let's parse a list with worker args
# Supposing it is stored in environment variable with a name we don't know now
# We know that this variable will be also put in environment as WORKER_ARGS_VAR
# Prepare parser for our list
ENV_LIST_STR = env.SeparatedList.factory(split_sep=",", parser=str)
# Define a Link variable
OPTIONAL_WORKER_ARGS: Variable[list[str]] = Link(
src=env.Var("WORKER_ARGS_VAR", str) >> hint.Required(False),
linker=ENV_LIST_STR
)
# WORKER_ARGS_VAR is not set and not required
# Link returns None:
assert OPTIONAL_WORKER_ARGS() is None
# Now, set WORKER_ARGS_VAR to empty string
os.environ["WORKER_ARGS_VAR"] = ""
# env.SeparatedList considers empty/whitespace source as empty list
# (natively, Python str.split implementation returns a list [""] with empty string)
assert OPTIONAL_WORKER_ARGS() == []
Custom lists:
import os
from pathlib import Path
from standby import List
from standby.env import Var
# PATH-like variables
os.environ["PLUGIN_PATHS"] = "/opt/plugins:/usr/local/plugins:/tmp/plugins"
PATH_LIST = List[Path, str, str](
src=Var("PLUGIN_PATHS", str),
splitter=lambda s: s.split(":"), # use ':' as a separator
parser=Path, # create a Path from each part
)
print(PATH_LIST()) # [PosixPath('/opt/plugins'), PosixPath('/usr/local/plugins'), PosixPath('/tmp/plugins')]
# empty string returns a list with single element as expected from Python str.split()
os.environ["PLUGIN_PATHS"] = ""
print(PATH_LIST()) # [PosixPath('.')]
Summary on semantics:
- If the source variable of a
ListorLinkis missing:- Without hints, a
VariableNotSeterror is raised; - With
Required(False)on the source, it returnsNone; - With
Default([...])on the result variable, the default is used.
- Without hints, a
Development
This project uses a src/ layout and provides optional development dependencies for testing and type checking.
Setup
Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
Install in editable mode with dev extras
pip install -e .[dev]
Run tests with coverage
Run tests:
pytest
By default, pytest is configured (via pyproject.toml) to:
- discover tests in the test/ directory,
- run with branch coverage for the standby package,
- show missing lines (term-missing)
To run without coverage flags, use:
pytest -q
Type checking
Run mypy on the package sources:
mypy
or explicitly:
mypy src/standby
Linting
Check linting with ruff:
ruff lint
Check and fix if possible:
ruff link --fix
Tox
Tox allows to run all checks on all supported Python versions:
tox
Make sure you have all Python versions installed before running Tox.
Recommended way is to use pyenv.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file standby_variables-1.0.0.tar.gz.
File metadata
- Download URL: standby_variables-1.0.0.tar.gz
- Upload date:
- Size: 8.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48418923182f7095974f2ce0dabf2d4ea2cfb049cd05ab094e2a3ba6bbe70724
|
|
| MD5 |
285e73b8bd061bac309a244fe3f773b4
|
|
| BLAKE2b-256 |
132ecc7a8bea1e6d64aa347c8d128af0f8582b5ff655445bea4cb81ea1f91857
|
Provenance
The following attestation bundles were made for standby_variables-1.0.0.tar.gz:
Publisher:
release.yml on ilia-khaustov/standby-variables
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
standby_variables-1.0.0.tar.gz -
Subject digest:
48418923182f7095974f2ce0dabf2d4ea2cfb049cd05ab094e2a3ba6bbe70724 - Sigstore transparency entry: 592241489
- Sigstore integration time:
-
Permalink:
ilia-khaustov/standby-variables@1e22969a330745764aa034e77e55b720cbcb130d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ilia-khaustov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1e22969a330745764aa034e77e55b720cbcb130d -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file standby_variables-1.0.0-py3-none-any.whl.
File metadata
- Download URL: standby_variables-1.0.0-py3-none-any.whl
- Upload date:
- Size: 10.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
799863fd11d10eaef806d14f6ccf1831bf96df4c6591ba4cd8b33dacbff814d2
|
|
| MD5 |
ebe71dd34342bc1f19783718d25840c2
|
|
| BLAKE2b-256 |
07f80e214e1cb2a7e10708176fe6dbea4b3aa8b75149f409a716add040d06909
|
Provenance
The following attestation bundles were made for standby_variables-1.0.0-py3-none-any.whl:
Publisher:
release.yml on ilia-khaustov/standby-variables
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
standby_variables-1.0.0-py3-none-any.whl -
Subject digest:
799863fd11d10eaef806d14f6ccf1831bf96df4c6591ba4cd8b33dacbff814d2 - Sigstore transparency entry: 592241492
- Sigstore integration time:
-
Permalink:
ilia-khaustov/standby-variables@1e22969a330745764aa034e77e55b720cbcb130d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ilia-khaustov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1e22969a330745764aa034e77e55b720cbcb130d -
Trigger Event:
workflow_dispatch
-
Statement type: