Comparisons¶
environ¶
The Python standard library os
module offers operating system utilities, including os.environ
for working with environment variables. os.environ
only handles strings, and doesn't include tools for file I/O.
os.environ
is instantiated from os._Environ
, which is an implementation of collections.abc.MutableMapping
. The source code for os.environ
can be found in python/cpython/Lib/os.py, and the source code for collections.abc.MutableMapping
can be found in python/cpython/Lib/_collections_abc.py. As explained in the docstring for MutableMapping
:
A MutableMapping is a generic container for associating key/value pairs.
This class provides concrete generic implementations of all methods except for
__getitem__
,__setitem__
,__delitem__
,__iter__
, and__len__
.
Subclasses of collections.abc.MutableMapping
, such as os._Environ
, need to provide implementations of each of those methods.
Type stubs for os.environ
are in python/typeshed/stdlib/os/__init__.pyi. Stubs for collections.abc.MutableMapping
are in python/typeshed/stdlib/typing.pyi (these are the type stubs for the standard library typing
module, which aliases collections.abc.MutableMapping
).
See the os
module docs and the docs on mapping types for more information.
environs¶
- environs is a project with useful features for managing .env files and application settings. Its API was inspired by django-environ and envparse (and the maintainers considered merging environs and envparse into one project). The README compares environs to
os.environ
, and justifies the additional features that environs provides. - While it separates config from code, as suggested by the twelve-factor app methodology, it also combines environment variables and settings. Environment variables and their type-casted setting counterparts are combined into the same model.
- Initially, the source code wasn't consistently type-annotated (sloria/environs#186), but based on its PEP 561 marker file, it appears to be type-annotated now.
- Depends on python-dotenv (sloria/environs#196), so it inherits the limitations described in the python-dotenv section.
pydantic¶
Settings configuration¶
pydantic offers a BaseSettings
model. Settings class attributes are automatically read from environment variables, and the full power of pydantic data parsing/validation can be applied.
Simple pydantic settings model
import os
from pydantic import BaseSettings
os.environ["BOOLEAN_SETTING"] = "false"
os.environ["INTEGER_SETTING"] = "123"
os.environ["STRING_SETTING"] = "example_value"
class SimpleSettings(BaseSettings):
boolean_setting: bool = True
integer_setting: int = 000
string_setting: str = "default_value"
print(SimpleSettings().dict())
# {"boolean_setting": False, "integer_setting": 123, "string_setting": "example_value"}
File I/O¶
- In addition to reading environment variables that have already been set, pydantic can load environment variables from .env files. However, it depends on python-dotenv to load .env files, so it inherits the limitations described in the python-dotenv section.
- If no .env file is found at the path provided, pydantic will fail silently, rather than raising a
FileNotFoundError
. This can lead to issues if applications depend on environment variables that pydantic fails to load.
python-decouple¶
- python-decouple loads settings from .env and .ini files. Its supported configuration file format appears to be inspired by Foreman, a Ruby configuration management tool.
- Variables are set with calls to instances of its
AutoConfig
class, which offers type-casting to convert strings to other Python types:config("DEBUG", cast=bool)
. - Source code is not type-annotated.
- Classes inherit from
object
, and therefore require their own implementations of methods already present in other data structures. This could be easily eliminated by inheriting from a mapping data structure such ascollections.abc.MutableMapping
. - Continues supporting Python 2 after its end-of-life, and has not been tested on the latest versions of Python 3.
python-dotenv¶
python-dotenv is a package for loading .env files and setting environment variables. It was started by Django co-creator Jacob Kaplan-Moss in 2013, and was originally called django-dotenv. It is used by Uvicorn and pydantic, and suggested in the FastAPI docs.
Environment variables¶
- Its primary data structure,
dotenv.main.DotEnv
, inherits fromobject
. As a result, it requires its own mapping methods (such asdict()
) that could be obviated by inheriting from a mapping data structure such ascollections.abc.MutableMapping
. - Other methods have confusing, counter-intuitive APIs. For example, the
load_dotenv()
function is supposed to "Parse a .env file and then load all the variables found as environment variables," according to its docstring. However, the function always returnsTrue
, even if no .env file is found or no environment variables are set, because ofDotEnv.set_as_environment_variables()
. Furthermore, this confusing behavior is not documented, because, as the maintainer commented, "The return value ofload_dotenv
is undocumented as I was planning to do something useful with it, but could not settle down to one."
File I/O¶
- Loads files with the synchronous
open()
built-in function. Async support is not provided. - Does not integrate with object storage like AWS S3.
Project maintenance¶
- Continued supporting Python 2 after its end-of-life (until 0.19.0), so it had to use Python 2 type comments and other legacy cruft.
- Maintainers have not been receptive to improvements (see theskumar/python-dotenv#263 for context).
Comparing fastenv and python-dotenv¶
DotEnv
¶
- Both fastenv and python-dotenv provide a
DotEnv
class for managing environment variables fastenv.DotEnv
inherits fromcollections.abc.MutableMapping
,dotenv.main.DotEnv
inherits fromobject
- fastenv includes
DotEnv
in its__all__
, python-dotenv does not (it must be directly imported fromdotenv.main
)
find_dotenv
¶
- fastenv:
await fastenv.find_dotenv()
(async) - python-dotenv:
dotenv.find_dotenv()
(sync) - Both fastenv and python-dotenv look for
".env"
by default - Both python-dotenv and fastenv return
os.PathLike
objects - fastenv raises
FileNotFoundError
exceptions by default if files are not found, python-dotenv does not
load_dotenv
¶
- fastenv:
await fastenv.load_dotenv()
(async) - python-dotenv:
dotenv.load_dotenv()
(sync) fastenv.load_dotenv
can load multiple .env files in a single call,dotenv.load_dotenv
cannotfastenv.load_dotenv
logs the number of environment variables loaded,dotenv.load_dotenv
does notfastenv.load_dotenv
returns aDotEnv
model,dotenv.load_dotenv
returnsTrue
(even if no .env file was found and no environment variables were loaded)
find_dotenv
with load_dotenv
¶
Users who would like to ensure their .env files are found, and log the result, should be aware that dotenv.load_dotenv
:
- Only calls
find_dotenv
if a file path is not provided, and does not pass an argument through tofind_dotenv
to raise exceptions if the file is not found - Requires a call to
DotEnv.set_as_environment_variables
to actually set environment variables - Does not provide logging
- Does not provide exception handling (its
verbose
argument does not necessarily raise an exception) - Does not return the
DotEnv
instance created byload_dotenv
, but always returnsTrue
, even if no .env file is found or no environment variables are set
Something like the following is therefore needed instead of using dotenv.load_dotenv
:
Finding and loading a .env file with python-dotenv
import logging
from dotenv import find_dotenv
from dotenv.main import DotEnv
def find_and_load_my_dotenv(env_file: str = ".env") -> DotEnv:
try:
logger = logging.getLogger()
source = find_dotenv(filename=env_file, raise_error_if_not_found=True)
dotenv = DotEnv(source, verbose=True)
dotenv.set_as_environment_variables()
logger.info(
f"Python-dotenv loaded {len(dotenv.dict())} variables from {env_file}"
)
return dotenv
except Exception as e:
logger.error(f"Error loading {env_file}: {e.__class__.__qualname__} {e}")
raise
The above effect can be accomplished with fastenv in a single call, await fastenv.load_dotenv(find_source=True)
. This call to fastenv.load_dotenv
:
- Finds the .env file (
find_source=True
) with itsfind_dotenv
method and the file name provided (".env"
by default), logging and raising aFileNotFoundError
if not found - Sets environment variables automatically
- Logs successes and errors automatically
- Raises exceptions by default
- Returns a
DotEnv
instance
dotenv_values
¶
- fastenv:
await fastenv.dotenv_values()
(async) - python-dotenv:
dotenv.dotenv_values()
(sync) fastenv.dotenv_values
offers afind_dotenv
argument to find files before loading and returning values,dotenv.dotenv_values
does notfastenv.dotenv_values
offers araise_exceptions
argument to determine whether or not exceptions will be raised,dotenv.dotenv_values
does not (itsverbose
argument does not necessarily raise an exception)fastenv.dotenv_values
logs successes and errors automatically,dotenv.dotenv_values
does not
Writing to .env files¶
- fastenv:
await fastenv.dump_dotenv()
(async, and writes an entireDotEnv
model to a file) - python-dotenv:
dotenv.get_key()
,dotenv.set_key()
,dotenv.unset_key()
(sync, and can only write single variables to a file)
Starlette¶
Settings configuration¶
Starlette offers a config module for working with environment variables and settings, which takes inspiration from python-decouple. Settings are created by calling a Starlette Config
instance. Constant notation is suggested for settings (UPPERCASE_WITH_UNDERSCORES
).
Example
import starlette.config
config = starlette.config.Config()
FOO_CONSTANT = config("FOO_VARIABLE", cast=str, default="baz")
Starlette will look for an environment variable FOO_VARIABLE
. For example, setting FOO_VARIABLE=bar
in the environment will result in FOO_CONSTANT = "bar"
. If the environment variable is unset, the result will be FOO_CONSTANT = "baz"
. The cast
keyword argument allows use of Starlette type-casting.
Type-casting¶
Type-casting provides improvements over some aspects of the standard library. For example, Starlette intuitively casts Boolean values, unlike the Python built-in bool
type:
Type-casting with starlette.config
.venv ❯ python3
>>> bool("false")
True
>>> import starlette.config
>>> config = starlette.config.Config()
>>> config("BOOLEAN_SETTING", cast=bool, default="false")
False
One-way configuration preference¶
Starlette has an opinionated one-way configuration preference (environment variables -> Starlette Config
instance). To avoid modifying environment variables after they have been loaded into a Starlette Config
instance, Starlette provides its own mapping onto os.environ
(starlette.config.environ
), which will raise an exception on attempts to change an environment variable that has already been loaded into a corresponding setting on a Config
instance.
While it is useful to have starlette.config.environ
synchronized with os.environ
, the downside is that starlette.config.environ
contains local environment variables loaded from os.environ
, and therefore wouldn't typically be dumped to a file.
It is also important to note that the one-way preference will only be enforced when using starlette.environ
. If a variable is changed using os.environ
, it will be updated correspondingly in starlette.environ
, but no exception will be raised, and the Config
instance value will not be updated.
Environment variables with starlette.config
.venv ❯ python3
>>> import os
>>> import starlette.config
>>> os.environ.get("FOO_VARIABLE")
>>> starlette.config.environ["FOO_VARIABLE"] = "bar"
>>> os.environ.get("FOO_VARIABLE")
'bar'
>>> config = starlette.config.Config()
>>> FOO_CONSTANT = config("FOO_VARIABLE", cast=str, default="baz")
>>> FOO_CONSTANT
'bar'
>>> starlette.config.environ["FOO_VARIABLE"] = "foo"
Traceback (most recent call last):
File ".venv/lib/python3.9/site-packages/starlette/config.py", line 26, in __setitem__
raise EnvironError(
starlette.config.EnvironError: Attempting to set environ['FOO_VARIABLE'],
but the value has already been read.
>>> os.environ["FOO_VARIABLE"] = "foo"
>>> os.environ["FOO_VARIABLE"]
'foo'
>>> starlette.config.environ["FOO_VARIABLE"]
'foo'
>>> FOO_CONSTANT
'bar'
File I/O¶
- Starlette
Config
accepts anenv_file
keyword argument, which should point to a .env file on disk. It loads the file with the synchronousopen()
built-in function. - If no .env file is found at the path provided by
Config(env_file)
, Starlette will fail silently, rather than raising aFileNotFoundError
. This can lead to issues if applications depend on environment variables that Starlette fails to load. - Starlette
Config
does not support multiple .env files (encode/starlette#432).
The future of starlette.config
¶
From encode/starlette#432:
I guess we may actually end up pulling the configuration stuff out into a strictly separate package (or indeed even just pointing at python-decouple - since it's the same style).
With 12-factor config you really should have a fairly small set of environment. (Eg. the database configuration should just be a single URL.)
What we will want to do though is provide really good examples of how to arrange things sensibly, so that eg. you have a project that has a
settings.py
that includes a whole bunch of project configuration, and which you can arrange into separate bits, and demonstrate clearly how the environment should be a small set of variables, but the project settings may be more comprehensive.We could perfectly well also point to other configuration-management packages that're out there as alternatives.
Other¶
- dotenvy can load .env files and apply a type schema to the variables. It does not appear to be actively maintained.
- env does not add much beyond
os.environ
(does not even load files), has not been released since 2012, and does not appear to be actively maintained. - envparse offers features for parsing and type-casting environment variables, but does not appear to be actively maintained.
- python-configurator depends on python-dotenv and appears to emphasize TOML settings files.