Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

env

import env

Configuration from a file, in the environment, in the type you wanted.

A program’s configuration belongs in its environment, and a developer’s machine has nowhere convenient to put it. env closes that gap: it reads a .env file into the process environment at startup, and it reads values back out already converted to the number, boolean or list the program is going to use.

import env

env.load()

var port = env.int('PORT', 8080)
var debug = env.bool('DEBUG', false)
var secret = env.require('SESSION_SECRET')

The file

A .env file sits beside the program and is not committed. One name per line:

# The address to bind on.
HOST=127.0.0.1
PORT=8080

DATABASE_URL="postgres://app:secret@${HOST}/app"
SESSION_SECRET='r4w $tring, taken exactly as written'

PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqh...
-----END PRIVATE KEY-----"

Unquoted values are trimmed and run to a comment or the end of the line. Double quotes resolve backslash escapes and allow $ references. Single quotes and backticks take every character exactly as written. Values in any of the three quotes may span lines. parse() states the grammar in full.

What already exists wins

A name that is already set in the process environment is left alone, and the file fills in the rest. This is the whole point of the default: the file carries what a developer needs to run the program at all, and production sets the real values through the shell, the orchestrator or the CI runner without the file having to know.

Result says exactly what happened, and override() reverses the rule where a file genuinely should win:

var result = env.loader().path('.env').override().load()

echo result.applied   # the names this load set
echo result.skipped   # the names that were already set

Throughout the module, a variable set to the empty string counts as unset. FLAG= in a file, an empty shell variable and a name nobody ever set all mean the same thing, which is the one thing anybody means by them.

References between values

Values may refer to other values and to the environment around them, with the shell’s syntax and three of its modifiers:

HOST=localhost
PORT=${PORT:-8080}
PUBLIC_URL=http://${HOST}:${PORT}
CACHE_DIR=${XDG_CACHE_HOME:-${HOME}/.cache}/app
SESSION_SECRET=${SESSION_SECRET:?the deploy must supply a session secret}

A reference resolves to the value the name will hold once the load has finished, so PORT=${PORT:-8080} reads “whatever the environment already says, and 8080 when it says nothing” no matter which line of the file defines it. \$ is a literal dollar sign, and a value in single quotes never expands at all.

Layering files

Sources are read in the order they are added, and the last one to define a name defines it:

env.loader()
  .path('.env')         # what the team shares, committed as .env.example
  .path('.env.local')   # what this machine does differently
  .load()

source() adds text instead of a file, so configuration that arrived from a secret store or a test fixture goes through the same parsing, expansion and precedence.

Reading values back

get() and require() answer with text. int(), float(), bool() and list() answer with the type and raise a ValueError naming the variable when the text is not one:

var port    = env.int('PORT', 8080)
var timeout = env.float('REQUEST_TIMEOUT', 2.5)
var debug   = env.bool('DEBUG', false)
var origins = env.list('CORS_ORIGINS', ',', [])

They read the process environment, not the file, so they answer the same whether a value came from .env or from the shell. That is what makes the same program run unchanged in both places.

Do not commit it

A .env file holds the values that differ between one deployment and the next, which is to say it holds the secrets. Commit a .env.example with every name and no real value, add .env to .gitignore, and let required() fail loudly on a machine where the file was never made.

The env API

Every public name in env, wherever it is declared. Each links to the page that documents it.

NameKindSummary
env.EnvErrorclassBase class for every error this module raises.
env.LoaderclassBuilds a load: the sources it reads and the four decisions it makes about them.
env.MissingFileclassA file the loader was told to require is not there.
env.MissingVariableclassA variable a program said it needed is not configured.
env.ParseErrorclassA source could not be parsed.
env.ResultclassWhat a load did.
env.boolfunctionReturns the value of name as a boolean.
env.expand.expandfunctionReplaces every $ reference in value with what resolve answers, and returns the result.
env.floatfunctionReturns the value of name as a number.
env.getfunctionReturns the value of name, or default_value when it is unset or empty.
env.hasfunctionReturns true when name is set to a non-empty value.
env.intfunctionReturns the value of name as an integer.
env.is_namefunctionReturns true when name is a legal environment variable name.
env.listfunctionReturns the value of name split into a list.
env.loadfunctionReads an environment file and writes what it holds into the process environment.
env.loaderfunctionReturns a new Loader, for a load that needs more than the default.
env.parsefunctionReads the text of an environment file and returns its names and values.
env.requirefunctionReturns the value of name, and raises when it is unset or empty.
env.stringifyfunctionTurns a dictionary of names and values into the text of an environment file.

Submodules

ModuleReached asSummary
env.errorsenv.errors.*Every error the env module raises, under one root.
env.expandimport env.expandResolving the $ references inside a value.
env.loadingenv.loading.*The loader: which sources to read, in what order, and what to do with the names that come out of them.
env.parserenv.parser.*The scanner that turns the text of an environment file into names and values, and the writer that turns them…
env.valuesenv.values.*Reading configuration back out of the process environment, in the type the program actually wants.

Functions

loader()

env.loader() -> Loader

Returns a new Loader, for a load that needs more than the default.

import env

var result = env.loader()
  .path('.env')
  .path('.env.local')
  .required()
  .load()

Returns Loader

load()

env.load(path: ?string) -> Result

Reads an environment file and writes what it holds into the process environment.

The one line most programs need, and the first line most of them run. path defaults to .env in the current working directory, a leading ~ is expanded, and a file that is not there leaves the environment as it stands.

Names already set to a non-empty value are left alone. Loader is where that, and everything else about a load, can be changed.

import env

env.load()
env.load('config/production.env')

Parameters

  • path (?string) — Defaults to '.env'.

Returns Result

Raises ParseError when the file is not a valid environment file.

Raises MissingVariable when a ${NAME:?reason} reference finds nothing.


2026, Richard Ore and Zuri contributors