
Research
SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains
An emerging npm supply chain attack that infects repos, steals CI secrets, and targets developer AI toolchains for further compromise.
environs
Advanced tools
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.
.env files
.env files into os.environ (useful for local development)pip install environs
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}
The following are all type-casting methods of Env:
env.strenv.boolenv.intenv.floatenv.decimalenv.list (accepts optional subcast and delimiter keyword arguments)env.dict (accepts optional subcast_keys, subcast_values, delimiter, and key_value_delimiter keyword arguments)env.jsonenv.datetimeenv.dateenv.timeenv.timedelta (assumes value is an integer in seconds, or an ordered duration string like 7h7s or 7w 7d 7h 7m 7s 7ms 7us)env.url
urllib.parse.ParseResult and therefore expects a ParseResult for its default.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 a pathlib.Path)env.enum (casts to any given enum type specified in enum keyword argument)
by_value=True to parse and validate by the Enum's values..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
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
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)
# 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
# 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.
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).
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
)
# 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}
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 PASSWORD_FILE=/run/secrets/password
env = FileAwareEnv()
password = env.str("PASSWORD") # => 'my secret password'
It's also possible to set a different suffix for the variable names:
from environs import FileAwareEnv
# printf 'my secret password' >/run/secrets/password
# export PASSWORD_SECRET=/run/secrets/password
env = FileAwareEnv(file_suffix="_SECRET")
password = env.str("PASSWORD") # => 'my secret password'
To strip leading and trailing whitespaces from the value:
from environs import FileAwareEnv
# printf ' value with leading and trailing whitespaces \n' >/run/secrets/password
# export PASSWORD_SECRET=/run/secrets/password
env: FileAwareEnv = FileAwareEnv(strip_whitespace=True)
password: str = env.str("PASSWORD") # => `value with leading and trailing whitespaces`
# export DOMAIN='http://myapp.com'
# export COLOR=invalid
from furl import furl
# Register a new parser method for paths
@env.parser_for("furl")
def furl_parser(value):
return furl(value)
domain = env.furl("DOMAIN") # => furl('https://myapp.com')
# Custom parsers can take extra keyword arguments
@env.parser_for("choice")
def choice_parser(value, choices):
if value not in choices:
raise environs.EnvError("Invalid!")
return value
color = env.choice("COLOR", choices=["black"]) # => raises EnvError
# myapp/settings.py
from environs import Env
env = Env()
env.read_env()
# Override in .env for local development
DEBUG = env.bool("FLASK_DEBUG", default=False)
# SECRET_KEY is required
SECRET_KEY = env.str("SECRET_KEY")
Load the configuration after you initialize your app.
# myapp/app.py
from flask import Flask
app = Flask(__name__)
app.config.from_object("myapp.settings")
For local development, use a .env file to override the default
configuration.
# .env
DEBUG=true
SECRET_KEY="not so secret"
Note: Because environs depends on python-dotenv,
the flask CLI will automatically read .env and .flaskenv files.
environs includes a number of helpers for parsing connection URLs. To install environs with django support:
pip install environs[django]
Use env.dj_db_url, env.dj_cache_url and env.dj_email_url to parse the DATABASE_URL, CACHE_URL
and EMAIL_URL environment variables, respectively.
For more details on URL patterns, see the following projects that environs is using for converting URLs.
Basic example:
# myproject/settings.py
from environs import Env
env = Env()
env.read_env()
# Override in .env for local development
DEBUG = env.bool("DEBUG", default=False)
# SECRET_KEY is required
SECRET_KEY = env.str("SECRET_KEY")
# Parse database URLs, e.g. "postgres://localhost:5432/mydb"
DATABASES = {"default": env.dj_db_url("DATABASE_URL")}
# Parse email URLs, e.g. "smtp://"
email = env.dj_email_url("EMAIL_URL", default="smtp://")
EMAIL_HOST = email["EMAIL_HOST"]
EMAIL_PORT = email["EMAIL_PORT"]
EMAIL_HOST_PASSWORD = email["EMAIL_HOST_PASSWORD"]
EMAIL_HOST_USER = email["EMAIL_HOST_USER"]
EMAIL_USE_TLS = email["EMAIL_USE_TLS"]
# Parse cache URLS, e.g "redis://localhost:6379/0"
CACHES = {"default": env.dj_cache_url("CACHE_URL")}
For local development, use a .env file to override the default
configuration.
# .env
DEBUG=true
SECRET_KEY="not so secret"
For a more complete example, see
django_example.py
in the examples/ directory.
See The 12-factor App section on configuration.
os.environ?While os.environ is enough for simple use cases, a typical application
will need a way to manipulate and validate raw environment variables.
environs abstracts common tasks for handling environment variables.
environs will help you
There are many great Python libraries for parsing environment variables. In fact, most of the credit for environs' public API goes to the authors of envparse and django-environ.
environs aims to meet three additional goals:
See this GitHub issue which details specific differences with envparse.
MIT licensed. See the LICENSE file for more details.
FAQs
simplified environment variable parsing
We found that environs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
An emerging npm supply chain attack that infects repos, steals CI secrets, and targets developer AI toolchains for further compromise.

Company News
Socket is proud to join the OpenJS Foundation as a Silver Member, deepening our commitment to the long-term health and security of the JavaScript ecosystem.

Security News
npm now links to Socket's security analysis on every package page. Here's what you'll find when you click through.