Using Dataclasses for Configuration in Python
eblocha
Posted on April 2, 2022
Introduction
Defining configuration schemes for python apps can be tricky. In this article, I will showcase a simple and effective way to make handling configuration easy, with only the standard library.
Format
For this method, any file format will work, as long as you can parse it into a python dict
.
A Simple Example
We'll make a simple example with this made-up config from wikipedia:
---
receipt: Oz-Ware Purchase Invoice
date: 2012-08-06
customer:
first_name: Dorothy
family_name: Gale
items:
- part_no: A4786
descrip: Water Bucket (Filled)
price: 1.47
quantity: 4
- part_no: E1628
descrip: High Heeled "Ruby" Slippers
size: 8
price: 133.7
quantity: 1
bill-to: &id001
street: |
123 Tornado Alley
Suite 16
city: East Centerville
state: KS
ship-to: *id001
specialDelivery: >
Follow the Yellow Brick
Road to the Emerald City.
Pay no attention to the
man behind the curtain.
We can parse this into python to get a dict:
config = {
"reciept": "Oz-Ware Purchase Invoice",
"date": "2012-08-06",
"customer": {
"first_name": "Dorothy",
"family_name": "Gale",
},
"items": [
{
"part_no": "A4786",
"descrip": "Water Bucket (Filled)",
"price": 1.47,
"quantity": 4,
},
{
"part_no": "E1628",
"descrip": "High Heeled \"Ruby\" Slippers",
"size": 8,
"price": 133.7,
"quantity": 1,
},
],
"bill-to": {
"street" : "123 Tornado Alley\nSuite 16",
"city": "East Centerville",
"state": "KS",
},
"ship-to": {
"street" : "123 Tornado Alley\nSuite 16",
"city": "East Centerville",
"state": "KS",
},
"specialDelivery": "Follow the Yellow Brick Road to the Emerald City. Pay no attention to the man behind the curtain.",
}
However, using this in the code is cumbersome. config["customer"]["first_name"]
is prone to error, and difficult to refactor.
Dataclasses
Dataclasses will make our life much easier. We define the config properties, sub-properties, and types in a config.py
file:
import typing as t
from dataclasses import dataclass
from datetime import date
@dataclass
class Item:
part_no: str
description: str
price: float
quantity: int
size: int = None
def __post_init__(self):
# Do some validation
if self.quantity <= 0:
raise ValueError("quantity must be greater than zero")
@classmethod
def from_dict(cls: t.Type["Item"], obj: dict):
return cls(
part_no=obj["part_no"],
description=obj["descrip"],
price=obj["price"],
quantity=obj["quantity"],
size=obj.get("size"),
)
@dataclass
class Customer:
first_name: str
family_name: str
@classmethod
def from_dict(cls: t.Type["Customer"], obj: dict):
return cls(
first_name=obj["first_name"],
last_name=obj["family_name"],
)
@dataclass
class Address:
street: str
city: str
state: str
@classmethod
def from_dict(cls: t.Type["Address"], obj: dict):
return cls(
street=obj["street"],
city=obj["city"],
state=obj["state"],
)
@dataclass
class Order:
reciept: str
date: date
customer: Customer
items: t.Sequence[Item]
bill_to: Address
ship_to: Address
special_delivery: str = None
@classmethod
def from_dict(cls: t.Type["Order"], obj: dict):
return cls(
receipt=obj["reciept"],
date=date(obj["date"]),
customer=Customer.from_dict(obj["customer"]),
items=[Item.from_dict(item) for item in obj["items"]),
bill_to=Address.from_dict(obj["bill-to"]),
ship_to=Address.from_dict(obj["ship-to"]),
special_delivery=obj.get("specialDelivery"),
)
Now, when we want to use the config in our application, we can simply do:
raw_config = {...}
config = Order.from_dict(raw_config)
config.customer.first_name
This method has a ton of benefits:
- We get code completion and type hints in the editor
- It's easier to maintain, since you only have to change a config property name in one place
- Can implement version reconciliation in the
from_dict
method - Refactoring is a breeze, since editors can auto-refactor class property names
- Allows you to define configurations with python code, since you can instantiate the dataclasses directly in a
settings.py
file, for example - It's testable:
import unittest
from .config import Order
class TestOrderConfig(unittest.TestCase):
def test_example_config(self):
raw_config = {...}
expected = Order(
customer=Customer(...),
...
)
self.assertEqual(Order.from_dict(raw_config), expected)
Hopefully you found this useful, and can use this method to clean up some of your projects!
Posted on April 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.