Skip to content

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.

.env.schema
# @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).

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 around
port = env["DB_PORT"] # int
debug = env["DEBUG"] # bool
# build your own redaction using SENSITIVE_KEYS
safe = {k: ("***" if k in SENSITIVE_KEYS else v) for k, v in env.items()}
Terminal window
varlock run -- python main.py

load_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.

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:

config.py
from env import load_env
env = load_env() # loaded once, typed as Env
anywhere.py
from config import env
port = env["DB_PORT"] # int

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:

Terminal window
varlock run -- uvicorn main:app --reload
varlock run -- poetry run pytest
varlock run -- python manage.py runserver

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:

settings.py
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()
config.py
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)