Environs
simplified environment variable parsing
Install / Use
/learn @sloria/EnvironsREADME
environs: simplified environment variable parsing
environs is a Python library for parsing environment variables. It allows you to store configuration separate from your code, as per The Twelve-Factor App methodology.
Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->- Features
- Install
- Basic usage
- Supported types
- Reading
.envfiles - Handling prefixes
- Variable expansion
- Validation
- Deferred validation
- URL schemes
- Serialization
- Reading Docker-style secret files
- Defining custom parser behavior
- Usage with Flask
- Usage with Django
- Why...?
- License
Features
- Type-casting
- Read
.envfiles intoos.environ(useful for local development) - Validation
- Define custom parser behavior
- Framework-agnostic, but integrates well with Flask and Django
Install
pip install environs
Basic usage
With some environment variables set...
export GITHUB_USER=sloria
export MAX_CONNECTIONS=100
export SHIP_DATE='1984-06-25'
export TTL=42
export ENABLE_LOGIN=true
export GITHUB_REPOS=webargs,konch,ped
export GITHUB_REPO_PRIORITY="webargs=2,konch=3"
export LOCATIONS="x:234 y:123"
export COORDINATES=23.3,50.0
export LOG_LEVEL=DEBUG
Parse them with environs...
from environs import env
env.read_env() # read .env file, if it exists
# required variables
gh_user = env("GITHUB_USER") # => 'sloria'
secret = env("SECRET") # => raises error if not set
# casting
max_connections = env.int("MAX_CONNECTIONS") # => 100
ship_date = env.date("SHIP_DATE") # => datetime.date(1984, 6, 25)
ttl = env.timedelta("TTL") # => datetime.timedelta(seconds=42)
log_level = env.log_level("LOG_LEVEL") # => logging.DEBUG
# providing a default value
enable_login = env.bool("ENABLE_LOGIN", False) # => True
enable_feature_x = env.bool("ENABLE_FEATURE_X", False) # => False
# parsing lists
gh_repos = env.list("GITHUB_REPOS") # => ['webargs', 'konch', 'ped']
coords = env.list("COORDINATES", subcast=float) # => [23.3, 50.0]
# parsing dicts
gh_repos_priorities = env.dict(
"GITHUB_REPO_PRIORITY", subcast_values=int
) # => {'webargs': 2, 'konch': 3}
# parsing dicts with different delimiters
locations = env.dict(
"LOCATIONS", subcast_values=int, delimiter=" ", key_value_delimiter=":"
) # => {'x': 234, 'y': 123}
Supported types
The following are all type-casting methods of Env:
env.strenv.boolenv.intenv.floatenv.decimalenv.list(accepts optionalsubcastanddelimiterkeyword arguments)env.dict(accepts optionalsubcast_keys,subcast_values,delimiter, andkey_value_delimiterkeyword arguments)env.jsonenv.datetimeenv.dateenv.timeenv.timedelta(assumes value is an integer in seconds, or an ordered duration string like7h7sor7w 7d 7h 7m 7s 7ms 7us)env.url- This returns a
urllib.parse.ParseResultand therefore expects aParseResultfor its default.
- This returns a
from urllib.parse import urlparse
from environs import env
MY_API_URL = env.url(
"MY_API_URL",
default=urlparse("http://api.example.com"),
)
If you want the return value to be a string, use env.str with validate.URL instead.
from environs import env, validate
MY_API_URL = env.str(
"MY_API_URL",
default="http://api.example.com",
validate=validate.URL(),
)
env.uuidenv.log_levelenv.path(casts to apathlib.Path)env.enum(casts to any given enum type specified inenumkeyword argument)- Pass
by_value=Trueto parse and validate by the Enum's values.
- Pass
Reading .env files
# .env
DEBUG=true
PORT=4567
Call Env.read_env before parsing variables.
from environs import env
# Read .env into os.environ
env.read_env()
env.bool("DEBUG") # => True
env.int("PORT") # => 4567
Reading a specific file
By default, Env.read_env will look for a .env file in current
directory and (if no .env exists in the CWD) recurse
upwards until a .env file is found.
You can also read a specific file:
from environs import env
with open(".env.test", "w") as fobj:
fobj.write("A=foo\n")
fobj.write("B=123\n")
env.read_env(".env.test", recurse=False)
assert env("A") == "foo"
assert env.int("B") == 123
Handling prefixes
Pass prefix to the constructor if all your environment variables have the same prefix.
from environs import Env
# export MYAPP_HOST=lolcathost
# export MYAPP_PORT=3000
env = Env(prefix="MYAPP_")
host = env("HOST", "localhost") # => 'lolcathost'
port = env.int("PORT", 5000) # => 3000
Alternatively, you can use the prefixed context manager.
from environs import env
# export MYAPP_HOST=lolcathost
# export MYAPP_PORT=3000
with env.prefixed("MYAPP_"):
host = env("HOST", "localhost") # => 'lolcathost'
port = env.int("PORT", 5000) # => 3000
# nested prefixes are also supported:
# export MYAPP_DB_HOST=lolcathost
# export MYAPP_DB_PORT=10101
with env.prefixed("MYAPP_"):
with env.prefixed("DB_"):
db_host = env("HOST", "lolcathost")
db_port = env.int("PORT", 10101)
Variable expansion
# export CONNECTION_URL=https://${USER:-sloria}:${PASSWORD}@${HOST:-localhost}/
# export PASSWORD=secret
# export YEAR=${CURRENT_YEAR:-2020}
from environs import Env
env = Env(expand_vars=True)
connection_url = env("CONNECTION_URL") # =>'https://sloria:secret@localhost'
year = env.int("YEAR") # =>2020
Validation
# export TTL=-2
# export NODE_ENV='invalid'
# export EMAIL='^_^'
from environs import env, validate, ValidationError
# built-in validators (provided by marshmallow)
env.str(
"NODE_ENV",
validate=validate.OneOf(
["production", "development"], error="NODE_ENV must be one of: {choices}"
),
)
# => Environment variable "NODE_ENV" invalid: ['NODE_ENV must be one of: production, development']
# multiple validators
env.str("EMAIL", validate=[validate.Length(min=4), validate.Email()])
# => Environment variable "EMAIL" invalid: ['Shorter than minimum length 4.', 'Not a valid email address.']
# custom validator
def validator(n):
if n <= 0:
raise ValidationError("Invalid value.")
env.int("TTL", validate=validator)
# => Environment variable "TTL" invalid: ['Invalid value.']
environs.validate is equivalent to marshmallow.validate, so you can use any of the validators provided by that module.
Deferred validation
By default, a validation error is raised immediately upon calling a parser method for an invalid environment variable.
To defer validation and raise an exception with the combined error messages for all invalid variables, pass eager=False to Env.
Call env.seal() after all variables have been parsed.
# export TTL=-2
# export NODE_ENV='invalid'
# export EMAIL='^_^'
from environs import Env
from marshmallow.validate import OneOf, Email, Length, Range
env = Env(eager=False)
TTL = env.int("TTL", validate=Range(min=0, max=100))
NODE_ENV = env.str(
"NODE_ENV",
validate=OneOf(
["production", "development"], error="NODE_ENV must be one of: {choices}"
),
)
EMAIL = env.str("EMAIL", validate=[Length(min=4), Email()])
env.seal()
# environs.EnvValidationError: Environment variables invalid: {'TTL': ['Must be greater than or equal to 0 and less than or equal to 100.'], 'NODE_ENV': ['NODE_ENV must be one of: production, development'], 'EMAIL': ['Shorter than minimum length 4.', 'Not a valid email address.']}
env.seal() validates all parsed variables and prevents further parsing (calling a parser method will raise an error).
URL schemes
env.url() supports non-standard URL schemes via the schemes argument.
from urllib.parse import urlparse
REDIS_URL = env.url(
"REDIS_URL", urlparse("redis://redis:6379"), schemes=["redis"], require_tld=False
)
Serialization
# serialize to a dictionary of simple types (numbers and strings)
env.dump()
# {'COORDINATES': [23.3, 50.0],
# 'ENABLE_FEATURE_X': False,
# 'ENABLE_LOGIN': True,
# 'GITHUB_REPOS': ['webargs', 'konch', 'ped'],
# 'GITHUB_USER': 'sloria',
# 'MAX_CONNECTIONS': 100,
# 'MYAPP_HOST': 'lolcathost',
# 'MYAPP_PORT': 3000,
# 'SHIP_DATE': '1984-06-25',
# 'TTL': 42}
Reading Docker-style secret files
Some values should not be stored in the environment. For this use case a commonly
used technique is to store the value (e.g., a password) in a file and set the path
to that file in an environment variable. Use FileAwareEnv in place of Env to automatically check for environment variables with the _FILE suffix. If the
file is found, its contents will be read and returned.
from environs import FileAwareEnv
# printf 'my secret password' >/run/secrets/password
# export PASSWOR
