Python
To use varlock with Python, install the standalone binary and run your app under varlock run — it loads and validates your env, then injects it into the process.
Add @generatePythonEnv to your schema to generate a small, self-contained module — a coerced Env TypedDict, a load_env() function that parses the injected __VARLOCK_ENV blob, and a SENSITIVE_KEYS constant.
# @generatePythonEnv(path=env.py)The file is regenerated automatically on varlock load and varlock run, or explicitly with varlock codegen. It has no dependencies, importing it has no side effects, and it imports on any Python 3.7+ (it uses from __future__ import annotations, so NotRequired/Literal are type-checker-only).
Reading values
Section titled “Reading values”Call load_env() once, then read keys off the dict — values are coerced (int/bool/etc.), not raw strings:
from env import load_env, SENSITIVE_KEYS
env = load_env() # call once, hold or pass it aroundport = env["DB_PORT"] # intdebug = env["DEBUG"] # bool
# build your own redaction using SENSITIVE_KEYSsafe = {k: ("***" if k in SENSITIVE_KEYS else v) for k, v in env.items()}varlock run -- python main.pyload_env() raises a clear error if __VARLOCK_ENV is missing (e.g. you forgot varlock run). Optional keys that are unset are absent from the dict (NotRequired), not present as None.
Sharing one instance
Section titled “Sharing one instance”Every load_env() call re-parses the blob. To avoid reloading, load once in a config module and share it — the idiomatic “settings module” pattern:
from env import load_env
env = load_env() # loaded once, typed as Envfrom config import env
port = env["DB_PORT"] # intRunning your server
Section titled “Running your server”varlock run wraps any command, so boot your ASGI server, task runner, or scripts through it — local dev, CI, and production all use the same schema:
varlock run -- uvicorn main:app --reloadvarlock run -- poetry run pytestvarlock run -- python manage.py runserverUsing a settings library
Section titled “Using a settings library”If you’d rather keep the settings library you already use, every key is also injected as a plain environment variable — so Pydantic Settings, environs, and friends work unchanged. They read the raw string values (e.g. "5432", "true"), so your settings class is what coerces them:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): model_config = SettingsConfigDict(extra="ignore")
app_env: str database_url: str openai_api_key: str
settings = Settings()from environs import Env
env = Env()# Do not call env.read_env() — varlock run already populated os.environ
DATABASE_URL = env.str("DATABASE_URL")DEBUG = env.bool("DEBUG", default=False)