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 runserverAuto-invoking varlock run from Python
Section titled “Auto-invoking varlock run from Python”Some scripts re-exec themselves under varlock run so callers do not have to wrap every invocation. Two things to get right: check __VARLOCK_RUN first so you do not recurse, and pick the interpreter path deliberately rather than passing sys.executable.
import osimport sys
if "__VARLOCK_RUN" not in os.environ: if sys.prefix != sys.base_prefix: bin_dir = "Scripts" if os.name == "nt" else "bin" exe = "python.exe" if os.name == "nt" else "python3" python = os.path.join(sys.prefix, bin_dir, exe) else: python = sys.executable os.execvp("varlock", ["varlock", "run", "--", python] + sys.argv)Use os.execvp so the varlock binary is resolved from PATH. os.execv needs an absolute path, so it fails for a normal install.
The interpreter path matters because varlock run passes through whatever executable path it is given. On Linux, sys.executable is usually the venv interpreter, so passing it works. On macOS with Homebrew Python, sys.executable is the fully resolved Cellar binary, not the venv symlink: re-execing that path means PEP 405 never finds pyvenv.cfg, the child leaves the venv, and the script fails in ways that look like a varlock problem. Rebuilding the path from sys.prefix keeps the child inside the venv on both platforms.
Use sys.prefix, not VIRTUAL_ENV. sys.prefix != sys.base_prefix is Python’s own check for whether the running interpreter is in a venv, while VIRTUAL_ENV only reflects what the shell activated. The two disagree whenever a script is invoked through another venv’s interpreter directly, and following VIRTUAL_ENV there would re-exec into the wrong environment.
Using 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)