Helpers for creating typed, IDE-friendly configs in Python using dataclasses and dacite
.
Consider a situation in which we're tasked with building a small web application. To keep things flexible, we'd like to make some options configurable, including the host address on which the application will listen.
To do so, we can create a simple (YAML) based config file that looks something like this:
host: "localhost"
port: 1234
In a naive approach, we would probably load this config file using something like PyYAML:
import yaml
with open("examples/config.yml") as file:
config = yaml.safe_load(file)
print(config)
However, a drawback of this approach is that it only gives us a dict of values. We have no garantee that what we loaded has the required keys and expected types, meaning things can easily break once we actually start running our application.
We can avoid these issues by using Python's dataclass functionality to create config classes with clearly defined fields and types:
from dataclasses import dataclass
@dataclass()
class Config:
host: str
port: int
This configuration class will be responsible for holding our configuration values. It also serves as a convenient reference for which configuration fields we expect and what their types will be!
We can use the dataclass as follows:
config: Config = Config(host="localhost", port=1234)
print(config.host, config.port)
Unfortunately, the standard dataclass implementation does not do any type checking, meaning that we can easily construct configs with invalid types. Moreover, we don't have any easy mechanism to load values from different config sources (e.g. files, environment, etc.) into this new data class.
dacite-config
aims to provide a flexible approach for loading config values into dataclass-based configs. This is acheived by providing a simple functional API that allow you to load and combine config values from different sources. The loaded values are then injected into your dataclass-based configs using the awesome dacite
library, which checks for missing values and whether values have the correct types.
As an example, the following code reads config values from a YAML file (using the read_yaml
function) and loads the resulting values into our Config
class using the load_config
function. Internally, load_config
calls dacite.from_dict
to create the dataclass instance and check the loaded values against the dataclass:
from dacite_config import read_yaml, load_config
config: Config = load_config(
read_yaml("examples/config.yml"),
config_class=Config,
)
print(config)
Besides this, you can also combine different config sources using the chain
function. For example, this code loads config values from a YAML file and combines them with any environment variables that match the prefix MYAPP_
:
from dacite_config import read_yaml, read_env, chain, load_config
config: Config = load_config(
chain(
read_yaml("examples/config.yml"),
read_env(prefix="MYAPP_")
),
config_class=Config,
)
print(config)
This allows you to build configs that load their values from a static file but can also be overridden by environment variables.
Of course, you can also load configs from multiple files. This allows you to, for example, split config values for different environments (dev/test/prod) across different files:
from dacite_config import read_yaml, chain, load_config
env = "prod"
config: Config = load_config(
chain(
read_yaml("examples/config.yml"),
read_yaml(f"examples/config.{env}yml")
),
config_class=Config,
)
print(config)
Of course, this is just a few small examples. Thanks to dacite
, you can also create much more complicated configs with nested configuration classes, etc. Checkout the documentation for more examples.
To get started, you can install dacite-config as follows:
python -m pip install git+https://github.com/jrderuiter/dacite-config.git
For more detailed use cases + API documentation, see: TODO.
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Report bugs at https://github.com/jrderuiter/dacite-config/issues.
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it.
Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it.
We could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such.
The best way to send feedback is to file an issue at https://github.com/jrderuiter/dacite-config/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)