Using Flask with Pydantic, new BFF's?
Introduction:
Flask is one of the popular options to help you build REST APIs over HTTP. Although there are other time-tested frameworks like Django and the new kid on the block FastAPI, Flask still holds relevance when we need to create an API fairly quickly.
What is the need for a Data Validation library?
Data is the new oil and hence processing and evaluating data becomes much more critical than it sounds. This is where the data validation library comes into the picture. They help us to write different validation rules in an easier-to-understand format as well as ultimately leading to less boilerplate as well in the long run.
What are my options?
Traditionally people have relied on using Marshmallow, which helped with object serialization and deserialization as well as along with data validation. People have also used various Flask-like frameworks having wrapper-specific validators, Flask-Restful and Flask-Restplus.
Why Pydantic is the Rage today?
Python is a dynamically typed programming language or better I say duck-typed programming language, so errors will only be visible while the code is in the actual execution phase rather than during the code compilation phase.
Using type hints drastically improves developer experience wrt to code debugging and development, something which is natural to static programming languages, for eg. C, Java, etc. So naturally Python along with other dynamic programming languages has also taken this natural path starting with Python 3.5 through PEP 484 (python enhancement proposal in short)
Pydantic enforces the same as it is powered by type hints.
Let's begin, shall we?
First, if we haven't done so earlier, we must create and activate our virtual environment. I have mine done earlier, which I will reuse.
flask-india@dev-pc flask_pydantic % . venv/bin/activate
(venv) flask-india@dev-pc flask_pydantic %
(venv) flask-india@dev-pc flask_pydantic % pip install pydantic flask
Collecting pydantic
Using cached pydantic-2.8.2-py3-none-any.whl (423 kB)
Collecting flask
Downloading flask-3.0.3-py3-none-any.whl (101 kB)
|████████████████████████████████| 101 kB 8.0 MB/s
Collecting annotated-types>=0.4.0
Using cached annotated_types-0.7.0-py3-none-any.whl (13 kB)
Collecting typing-extensions>=4.6.1
Using cached typing_extensions-4.12.2-py3-none-any.whl (37 kB)
Collecting pydantic-core==2.20.1
Using cached pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl (1.7 MB)
Collecting Werkzeug>=3.0.0
Downloading werkzeug-3.0.4-py3-none-any.whl (227 kB)
|████████████████████████████████| 227 kB 12.7 MB/s
Collecting Jinja2>=3.1.2
Downloading jinja2-3.1.4-py3-none-any.whl (133 kB)
|████████████████████████████████| 133 kB 11.1 MB/s
Collecting click>=8.1.3
Downloading click-8.1.7-py3-none-any.whl (97 kB)
|████████████████████████████████| 97 kB 9.3 MB/s
Collecting blinker>=1.6.2
Downloading blinker-1.8.2-py3-none-any.whl (9.5 kB)
Collecting itsdangerous>=2.1.2
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Collecting importlib-metadata>=3.6.0
Downloading importlib_metadata-8.4.0-py3-none-any.whl (26 kB)
Collecting zipp>=0.5
Downloading zipp-3.20.0-py3-none-any.whl (9.4 kB)
Collecting MarkupSafe>=2.0
Downloading MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl (18 kB)
Installing collected packages: zipp, typing-extensions, MarkupSafe, Werkzeug, pydantic-core, Jinja2, itsdangerous, importlib-metadata, click, blinker, annotated-types, pydantic, flask
Successfully installed Jinja2-3.1.4 MarkupSafe-2.1.5 Werkzeug-3.0.4 annotated-types-0.7.0 blinker-1.8.2 click-8.1.7 flask-3.0.3 importlib-metadata-8.4.0 itsdangerous-2.2.0 pydantic-2.8.2 pydantic-core-2.20.1 typing-extensions-4.12.2 zipp-3.20.0
You can verify your installation of libraries as follows:
(venv) flask-india@dev-pc flask_pydantic % pip freeze
annotated-types==0.7.0
blinker==1.8.2
click==8.1.7
Flask==3.0.3
importlib_metadata==8.4.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
pydantic==2.8.2
pydantic_core==2.20.1
typing_extensions==4.12.2
Werkzeug==3.0.4
zipp==3.20.0
Save your dependencies to a file for future reuse as follows:
(venv) flask-india@dev-pc flask_pydantic % pip freeze > requirements.txt
Writing our Flask App with Pydantic
Let us create a sample python file that will contain the API logic.
(venv) flask-india@dev-pc flask_pydantic % vim flask_pydantic.py
Note: We are using Python 3.9 here on our system. Your imports may vary depending on the case of the Python version in your system.
# Import Modules:
import json
from datetime import datetime
from flask import Flask, request
from pydantic import BaseModel, ValidationError
app = Flask(__name__)
class BlogUser(BaseModel):
id: int
age: int
name: str = 'Flask India'
headers = {'Content-Type': 'application/json'}
@app.route('/v1/check-user', methods=['POST'])
def check_user():
try:
post_data = request.get_json()
validated_data = BlogUser(**post_data)
validated_data = validated_data.model_dump()
return validated_data, 200, headers
except ValidationError as e:
return e.errors(), 400, headers
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)
Let's run our app using the command below. You should see the below output if your app has booted up correctly.
(venv) flask-india@dev-pc flask_pydantic % python flask_pydantic.py
* Serving Flask app 'flask_pydantic'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
* Running on http://192.168.0.115:8000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 354-130-657
Let's test our API using curl requests.
flask-india@dev-pc flask_pydantic % curl --location 'http://localhost:8000/v1/check-user' \
--header 'Content-Type: application/json' \
--data '{}'
[
{
"input": {},
"loc": [
"id"
],
"msg": "Field required",
"type": "missing",
"url": "https://errors.pydantic.dev/2.8/v/missing"
},
{
"input": {},
"loc": [
"age"
],
"msg": "Field required",
"type": "missing",
"url": "https://errors.pydantic.dev/2.8/v/missing"
},
{
"input": {},
"loc": [
"name"
],
"msg": "Field required",
"type": "missing",
"url": "https://errors.pydantic.dev/2.8/v/missing"
}
]
In the above sample, we have tested with empty data in json body and we immediately see the pydantic validation in action highlighting our missing fields which are expected in our REST API json body.
Next, let's test again with some of the data being passed and 1/2 parameters missing as is the case usually.
flask-india@dev-pc flask_pydantic % curl --location 'http://localhost:8000/v1/check-user' \
--header 'Content-Type: application/json' \
--data '{"id": 1, "age": 23}'
[
{
"input": {
"age": 23,
"id": 1
},
"loc": [
"name"
],
"msg": "Field required",
"type": "missing",
"url": "https://errors.pydantic.dev/2.8/v/missing"
}
]
We see that we get the error message response from our REST API stating the field missing is 'name'.
Lets test again with correct data and see the API response.
flask-india@dev-pc flask_pydantic flask_pydantic % curl --location 'http://localhost:8000/v1/check-user' \
--header 'Content-Type: application/json' \
--data '{"id": 1, "age": 23, "name": "flask india"}'
{
"age": 23,
"id": 1,
"name": "flask india"
}
Conclusion
This is a brief introduction to how to use pydantic for data validation using Flask as the framework of choice for developing REST APIs. You can ofcourse do much more complex validation to support your usecase.
Subscribe to my newsletter
Read articles from Flask India directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Flask India
Flask India
We are a bunch of Developers who started to mentor beginners who started using Python and Flask in general in the Flask India Telegram Group. So this blog is a way to give it back to the community from where we have received guidance when we started as beginners.