New Python Library: fancy_dataclass

Posted on Oct 20, 2024 in Python

I've written a Python library called fancy_dataclass, a versatile Python library built around dataclasses. Python 3.7 introduced the dataclasses module, which lets you write "statically typed" classes using the type hinting mechanism.

The goal of this library is to enable type-driven development, which leverages the type system to eliminate a lot of boilerplate code. The idea is that you write a dataclass and then imbue with with "special powers." One common use case is to enable automatic conversion of Python objects to and from JSON. Another is to define a bundle of parameters and then expose them to a command-line argument parser. fancy_dataclass makes it possible to do these things with very few lines of code.

I am eager to have people try using it and provide feedback, so please check it out!

To install the library, do: pip install fancy_dataclass

It should support Python versions 3.8–3.12.

📝 Read the documentation here.

🪲 Submit bug reports or feature requests on Github.

Example 1: JSON Serialization

Let's define a dataclass that can be converted to and from JSON.

from dataclasses import dataclass
from fancy_dataclass import JSONDataclass

@dataclass
class Person(JSONDataclass):
    name: str
    age: int
    height: float
    hobbies: list[str]

Then we can easily convert a Person to a Python dictionary or a JSON string.

# create a Person
>>> person = Person(
    name='John Doe',
    age=47,
    height=71.5,
    hobbies=['reading', 'juggling', 'cycling']
)

# convert to Python dict
>>> person.to_dict()
{'name': 'John Doe',
 'age': 47,
 'height': 71.5,
 'hobbies': ['reading', 'juggling', 'cycling']}

# convert to JSON string
>>> print(person.to_json_string(indent=2))

{
  "name": "John Doe",
  "age": 47,
  "height": 71.5,
  "hobbies": [
    "reading",
    "juggling",
    "cycling"
  ]
}

It's easy to convert in the other direction, too:

>>> person = Person.from_json_string('{"name": "John Doe", "age": 47, "height": 71.5, "hobbies": ["reading", "juggling", "cycling"]}')
>>> person
Person(name='John Doe', age=47, height=71.5, hobbies=['reading', 'juggling', 'cycling'])

fancy_dataclass supports serialization of TOML as well as JSON, which is useful for configuration management.

Example 2: CLI Argument Parsing

Let's use fancy_dataclass to write a simple command-line program, greet.py:

from dataclasses import dataclass, field

from fancy_dataclass import CLIDataclass

@dataclass
class Greet(CLIDataclass):
    """A program to greet the user."""
    name: str = field(
        metadata={
            'help': 'name of person to greet'
        }
    )
    num_exclamations: int = field(
        default=1,
        metadata={
            'args': ['-n', '--num-exclamations'],
            'help': 'number of exclamation points'
        }
    )
    fancy: bool = field(
        default=False,
        metadata={
            'help': 'greet fancily'
        }
    )

    def run(self) -> None:
        # implement your main program logic
        if self.fancy:
            greeting = 'Greetings and salutations'
        else:
            greeting = 'Hello'
        exclamations = '!' * self.num_exclamations
        print(f'{greeting}, {self.name}{exclamations}')


if __name__ == '__main__':
    Greet.main()

This will create a full-fledged command-line program with argument parsing, without having to write any of it manually. You can view the help menu with:

python greet.py --help

Which prints out:

usage: greet.py [-h] [-n NUM_EXCLAMATIONS] [--fancy] name

A program to greet the user.

positional arguments:
  name                  name of person to greet

options:
  -h, --help            show this help message and exit
  -n NUM_EXCLAMATIONS, --num-exclamations NUM_EXCLAMATIONS
                        number of exclamation points
  --fancy               greet fancily

Note that fancy_dataclass uses the dataclass field metadata to construct the appropriate argument names and help strings for this menu.

Let's try running the program with a few different arguments:

$ python greet.py Bob
Hello, Bob!

$ python greet.py Alice --fancy
Greetings and salutations, Alice!

$ python greet.py Alice --fancy -n 3
Greetings and salutations, Alice!!!

$ python greet.py
usage: greet.py [-h] [-n NUM_EXCLAMATIONS] [--fancy] name
greet.py: error: the following arguments are required: name

We see that dataclass fields with a default value are optional, while those without a default are required.

Other Features

In addition to the examples above, fancy_dataclass can do much more, including:

  • Configuration management: store global configurations and use them anywhere in your program.
  • SQL persistence: define SQL tables, and save/load objects from a database.
  • Subprocess calls: generate command-line arguments to be passed to another program.

You are free to combine the features together so that the same dataclass can be used for multiple purposes (e.g. both SQL and JSON representation).

There is also a system for adjusting class-specific or field-specific settings (e.g. for JSON serialization, whether to suppress None values or default values); see the documentation for details.

What about pydantic?

Some of the features in this library, like JSON serialization, are also available in the popular pydantic library. pydantic also provides field validation, which fancy_dataclass does not (yet) do, and a host of other features. On the other hand, fancy_dataclass has features that pydantic does not have, like command-line parsing.

While fancy_dataclass does overlap with pydantic in some ways, it was designed to be as lightweight as possible, both in terms of dependencies and configurations. Another main difference is that pydantic classes must inherit from BaseModel (or use a special pydantic.dataclasses.dataclass decorator), while fancy_dataclass uses the ordinary dataclass decorator with additional "mixin" classes like JSONDataclass, CLIDataclass, etc. depending on what features you want to use.

You can also use both fancy_dataclass and pydantic together to get the best of both worlds.

Conclusion

Anyway, I hope you enjoy using this library, and I am always happy to respond to any bug reports and feature requests! Feel free to follow the project on Github or e-mail me at jeremys@nessiness.com.