Parsing Meta Blocks using Python's dataclass

Oct 10, 2018

Class Meta is a ubiquitous Python pattern that’s often used to define class-specific attributes in a framework.

For example, Django models use the Meta pattern to store model-specific attributes, and django-rest-framework uses it to specify serializer options.

This is a powerful pattern, but parsing options can get redundant. I was recently prototyping a system and found myself spending more type writing type-checks and option validation than I did actually solving the problem at hand. This inspired me to come up with a simple design pattern to reduce complexity. This pattern leverages Python’s new excellent dataclass construct.

Here’s an example:

class Model:
    @dataclass
    class MetaParser:
        quantity: int=0
        translations: field(default_factory=dict)

    class Meta:
        quantity = 123
        translations = { 'foo': 'bar' }

    def __init__(self):
        self.meta = None
        if hasattr(self, 'Meta'):
            data = {
                k: v for k, v in self.Meta.__dict__.items()
                if not k.startswith('__') and not callable(k)
            }
            self.meta = self.MetaParser(**data)

What this does:

  1. Ensures that valid arguments are passed and that they’re of the correct type. If, for example, quantity was a string, your program would throw a TypeError.
  2. Ensures that no invalid options are passed. If, for example, we added an extraneous option in our class Meta block, the program would also throw a TypeError.

All this in ~10 lines of code. This pattern is especially useful when dealing with tall inheritance chains and/or metaclasses.