Simple configuration with environment variables and pydantic, without repeating yourself!
pip install dryenv
For example, instead of writing:
# settings.py
import os
DATABASE_HOST = os.getenv("DATABASE_HOST", "localhost")
DATABASE_USERNAME = os.getenv("DATABASE_USERNAME", "admin")
DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD", "secretpassword")
DATABASE_TIMEOUT = int(os.getenv("DATABASE_TIMEOUT", 10))
DATABASE_PERSIST_CONNECTION = os.getenv("DATABASE_PERSIST_CONNECTION", "true").lower() == "true"
# database.py
import settings
connection = connect(
host=settings.DATABASE_HOST,
username=settings.DATABASE_USERNAME,
password=settings.DATABASE_PASSWORD,
timeout=settings.DATABASE_TIMEOUT,
persist_connection=settings.DATABASE_PERSIST_CONNECTION,
)
write:
# settings.py
from dryenv import DryEnv
class DATABASE(DryEnv):
HOST = "localhost"
USERNAME = "admin"
PASSWORD = "secretpassword"
TIMEOUT = 10
PERSIST_CONNECTION = True
# database.py
from settings import DATABASE
connection = connect(
host=DATABASE.HOST,
username=DATABASE.USERNAME,
password=DATABASE.PASSWORD,
timeout=DATABASE.TIMEOUT,
persist_connection=DATABASE.PERSIST_CONNECTION,
)
or even:
# settings.py
from dryenv import DryEnv
class DATABASE(DryEnv):
# Looking up environment variables is case-insensitive
host = "localhost"
username = "admin"
password = "secretpassword"
timeout = 10
persist_connection = True
# database.py
from settings import DATABASE
connection = connect(**DATABASE.dict())
DryEnv
is a thin wrapper around pydantic.BaseSettings
, which does most of the heavy lifting. DryEnv
makes things a little neater and more convenient by automatically:
- Setting
env_prefix
based on the class name, unless the class name isRoot
(case insensitive) in which case the prefix is empty. - Instantiating the class to trigger the environment lookups.
For example, this:
from dryenv import DryEnv
class DATABASE(DryEnv):
HOST = "localhost"
USERNAME = "admin"
PASSWORD = "secretpassword"
TIMEOUT = 10
PERSIST_CONNECTION = True
is roughly equivalent to:
from pydantic import BaseSettings
class DATABASE(BaseSettings):
class Config:
env_prefix = "DATABASE_"
HOST = "localhost"
USERNAME = "admin"
PASSWORD = "secretpassword"
TIMEOUT = 10
PERSIST_CONNECTION = True
DATABASE = DATABASE()
Here are the most important points about what pydantic provides:
- You can omit the default value and just declare a variable with a type annotation, e.g.
HOST: str
. This makes the setting required. - Variables will be parsed based on their type, which is determined by the annotation or the default value.
- For most simple field types (such as int, float, str, etc.), the environment variable value is parsed the same way it would be if passed directly to the initialiser (as a string). Booleans are parsed more intelligently, see here. Complex types like list, set, dict, and sub-models are populated from the environment by treating the environment variable's value as a JSON-encoded string.
For more information read the pydantic documentation.
This package could quite easily be part of pydantic itself. If you'd like that, vote on the issue here.
You can override the automatic env_prefix
setting by either:
- Naming your class
Root
(case insensitive) in which case the prefix is empty, or - Setting
env_prefix
as normal under theConfig
class.
You can turn off the automatic instantiation by setting auto_init = False
in the Config
.
You can instantiate DryEnv
yourself with your own constructor arguments by simply calling it as if it were the class. You can also access the class itself as normal with type()
.
The instance method DryEnv.prefixed_dict()
is similar to pydantic's dict()
, but the env_prefix
is included in the keys, so they match the original environment variable names.
For example:
class DATABASE(DryEnv):
HOST = "localhost"
USERNAME = "admin"
assert DATABASE.dict() == {"HOST": "localhost", "USERNAME": "admin"}
assert DATABASE.prefixed_dict() == {"DATABASE_HOST": "localhost", "DATABASE_USERNAME": "admin"}
The function populate_globals()
will search for instances of DryEnv
in the global variables in the calling context and then update the global variables with the prefixed_dict()
of those DryEnv
isntances. For example, if you called populate_globals()
after the example above, DATABASE_HOST
and DATABASE_USERNAME
would become global variables. This is useful in e.g. Django where settings need to be declared at the global level. You can pass your own dict for the function to use instead of the current global variables.
If you use PyCharm with the Django integration, it's able to intelligently inspect and navigate to values in django.conf.settings
...most of the time. For some reason a class declared in settings.py
doesn't work, so you can't navigate to the definition of a DryEnv
or autocomplete its values. To work around this, I suggest you:
- Declare appropriate settings in a different file e.g.
simple_settings.py
. - Import values from there in your apps instead of
django.conf.settings
so that PyCharm understands them. - In your
settings.py
, writefrom simple_settings import *
and callpopulate_globals()
in one of the settings files. This will allow Django and libraries to find settings likeDEBUG
andSECRET_KEY
at the global level while letting you define them withdryenv
and then forgetting about them.
Alternatively, you can add the line DATABASE = DATABASE
or DATABASE = DATABASE()
and then PyCharm will recognise this as a normal variable instead of a class.