
Python dataclasses are fantastic. Pydantic is fantastic. It is a tough choice if indeed we are confronted with choosing one or the other. I would say that comparing these two great modules is like comparing pears with apples, albeit similar in some regards, different overall.
Pydantic’s arena is data parsing and sanitization, while dataclasses a is a fast and memory-efficient (especially using slots, Python 3.10+) general-purpose data container. Check out this story, where I thoroughly compared Python data containers, including pydantic and dataclasses.
However, sometimes, it seems some code dependency is trying to make us choose. A great example is when using Fastapi; it is built on pydantic. If our app is simple, we may just as well do all the data models in pydantic and avoid any compatibility issues. Another example is when we inherit a codebase that is already using dataclasses. Why should we miss on dataclasses performance when using FastAPI? How can we convert our dataclasses schema to pydantic when we need to implement a web API using FastAPI. By the way, FastAPI is fantastic too.
In this story, we use some simple tricks to convert dynamically flat dataclasses into pydantic and vice-versa. Finally, we will test our implementation of such practical conversion with a FastAPI.
Story Structure
- dataclass (no defaults) into Pydantic
- dataclass with defaults into pydantic
- pydantic (no defaults) into dataclass
- pydantic with defaults into dataclass
- Building the tools: dataclass to pydantic
- Building the tools: pydantic to dataclass
- FastAPI example
dataclass (no defaults) to pydantic
The problem:
To solve this, we use pydantic’s utility to create models dynamically, _pydantic.createmodel. We need to supply this function with a name for our dynamically created model. We also need to provide kwargs (each keyword argument is the attribute’s name in the dynamically created model) with a tuple of attribute type and default value (if there is no default value, then use … instead).
To get the information we need for the pydantic model, we use the fields utility in the dataclasses module; using fields, we can access the attributes’ properties.
Now we can create an instance of our dynamically generated pydantic model:
printing the model and its type: name=’Diego’ age=33 <class ‘pydantic.main.DynamicPydanticPerson’>
So we are golden here.
pydantic (no defaults) into dataclass
The problem:
We now use dataclasses utility to create classes dynamically (_dataclasses.makedataclass), which __ requires the name of the dynamically created class and a list with tuples for each attribute; Each tuple should contain the attribute’s name and type.
We can check if the class is, in fact, a dataclass with _isdataclass from the dataclasses module. So we do that and instantiate the class:
prints: True DynamicPerson(name=’Diego’, age=33)
That is the output we expected.
dataclass with defaults to pydantic
Now we turn to the case where our dataclass has default values, and we want to convert it to pydantic.
The problem:
The idea is similar to the no defaults case; however, now we need to replace … for the default value. Furthermore, to check if the default value in the field of the dataclass is missing (no default value), we check if the field default is an instance of the __MISSINGTYPE class from the dataclasses module (kind of a hack here, but we need to get things done).
We continue to test our DynamicPydanticPerson:
printing the dataclasses and their types:
using the default: age=33 name=’Jane’ <class ‘pydantic.main.DynamicPydanticPerson’>
override the default: age=33 name=’Diego’ <class ‘main.PydanticPerson’>
Exactly what we expected.
pydantic with defaults into dataclass
The problem:
notice the default fields.
This case is very similar to the one where there are no defaults. If an attribute has a default value, the tuple per attribute has three elements instead of two, the third element being the default value (_field.required means no default):
testing that, in fact, it is a dataclass and the instances for default and default override:
prints: True DynamicPerson(age=33, name=’John’) DynamicPerson(age=33, name=’Diego’)
We are good to go.
Building the tools: dataclass to pydantic
Our code snippets for converting dataclasses to pydantic models were successful, with defaults and no defaults. Now it is time to wrap them up in a more usable tool. A function that does the conversion for us:
Let’s test it. The target dataclass:
Converting it to pydantic:
prints:
PydanticPerson
age=33 name=’Jane’
Notice that the name is taken from the name of the original dataclass (Person) and the "Pydantic" prefix is added via an f string in the converting function. There is the option to supply a custom name as well.
Building the tools: pydantic to dataclass
The function for converting dataclasses to pydantic:
Let’s test it. The target pydantic model:
Converting it to dataclass:
prints:
DataClassPerson
DataClassPerson(age=33, name=’John’)
Notice that the name is taken from the name of the original pydantic model class (Person) and the "DataClass" prefix is added in the converting function. This is the default behavior, alternatively a custom name can be passed to the converting function.
FastAPI example
At last, an example using FastAPI. This simple example shows how easy it is to use a dataclass with FastAPI. To make this example work, save the code from the two previous sections into a file named "dataclass_to_pydantic.py" and place it in the same directory where you run the following example. The following example should be named "fast_api_example.py", this is very important for the uvicorn server to run from within the script.
We create a dataclass FooQuery and then convert it to pydantic, which is passed as the type for the foo endpoint:
To test it, we execute the script and use the requests module to do a simple test of the foo endpoint:
Everything works as expected.
Final Words
Now we know how to convert in a simple way flat pydantic to dataclasses and vice-versa. Such conversions may be handy in several situations, including using dataclasses with FastAPI.
However, I would argue to first think carefully about the architecture of your app and try to avoid unnecessary conversions. Both pydantic and dataclasses have a lot of features and customizations, most of which would get lost on translation.
Finally, regarding nested models, their conversion is tricky. Using the tools in the code presented in the story is very straightforward to convert to an instance of the nested model. But to convert into a type (the uninstantiated class), we must first declare all of the nested models’ conversions. As I said, that part is tricky.
I hope this story was useful for you. Subscribe if you’d like more stories like this.
Liked the story? Support my writing by becoming a Medium member through my referral link below. Get unlimited access to my stories and many others.