FastAPI by A Python Beginner (2)
Typing & Data Validation
This is a multiparter journey. I don’t know where it’s going to ends. You’re always welcome if you’re interested in this topic or want to join this step-by-step journey.
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
Basic Typing in Python
Ever since I start using Typescript for frontend development, I promised myself I’ll never go back… Although the good old JavaScript still kicking in the codebase from my day job.
Python is a dynamically typed language, just like JavaScript. According to the official python documentation, the typing system was introduced in Python 3.5. Which makes Python “feels” like a statically typed language to work with, just like TypeScript.
So far, I have only tried some built-in types, which feels very comprehensive already.
a: int = 1
b: float = 1.0
c: bool = True
d: str = "string"
e: bytes = b"data"
Here’s some basic types, I found the float
type and bytes
type pretty sweet. As TypeScript only has number
to represent all the numbers, doesn’t care about it’s an integer or floating-point number. The bytes
type seems like a good addition for data transmission.
a: list[int] = [1]
b: set[int] = {1, 2}
c: dict[str, int] = {"count": 1}
d: tuple[str, int, float] = ("string", 1, 1.0)
e: tuple[int, ...] = [1, 2, 3, 4] # tuple with variable size
To properly type the built-in data structures seems pretty straightforward too.
a: int | str = 1
b: int | str = "string"
c: int | None = None # a union type with None makes it optional
And the union
or optional
type can be done with |
.
With these basic types above, we can move on and start working on our first API path operation (the hello world doesn’t really count).
Data Validation with Pydantic
Remember last time we installed a couple of dependencies to our docker image?
fastapi==0.103.1
pydantic==2.3.0
uvicorn==0.23.2
We’ve known this whole journey is about FastAPI. And we’ve use uvicorn
command to run our server. There’s only one thing remains unknown, the Pydantic.
Pydantic is a validation library for Python. I’ve been using quite a few JavaScript/TypeScript validation libraries. I have to say Pydantic is just so beautifully done. It just works. Period.
To validate the data, we first need a new endpoint that takes user’s input. At this point, I’m thinking about maybe I can make a tiny music player application. So, I’ll need a new Track class…
from pydantic import BaseModel
class Track(BaseModel):
title: str
artist: str
album: str | None = None
year: int
label: str | None = None
We first import BaseModel
from Pydantic, and create a Track class that inherit the BaseModel
. I’m trying not to make it too complicated at this stage. So, currently my Track
only have some simple properties. As you can see, I make album
and label
properties as optional
since it can be str
or None
. And by adding a = None
makes it default as None
.
Now, it’s time to create a /tracks
endpoint.
@app.post("/tracks")
async def create_track(track: Track):
return track
This /tracks
endpoint takes user’s request body track
which’s type is our Track
class. And it doesn’t do anything really, it just returns the user’s input as response.
Now if we go to http://localhost/docs
in the browser, we should be able to see something like this.

If you toggle open the Track
accordion under the Schemas
section…

You can see the title
, artist
and year
are marked as required. And the album
and label
are optional
just like what we want it to be.
Now toggle open the green post /tracks
accordion. and hit the “Try it out” button. You can see there’s a snippet of valid example request body. If we execute it directly, it will return a 200
OK, with all the meaningless example request body in the response.

Try to remove the artist
and change the title
as true
and hit “Execute”. Now we got a 422
unprocessable entity with the following response.
{
"detail": [
{
"type": "string_type",
"loc": [
"body",
"title"
],
"msg": "Input should be a valid string",
"input": true,
"url": "https://errors.pydantic.dev/2.3/v/string_type"
},
{
"type": "missing",
"loc": [
"body",
"artist"
],
"msg": "Field required",
"input": {
"title": true,
"album": "string",
"year": 0,
"label": "string"
},
"url": "https://errors.pydantic.dev/2.3/v/missing"
}
]
}
It’s probably not the most frontend friendly error message. But it does point out all the errors (inside the loc
property) and the reason what causes the errors (we can figure it out with the type
and msg
).
For now, the format of the error is not really my concern. So, I’ll keep it this way.
We have a way to create new track now, why stop it here? Let’s make a quick prototype of get
operation to retrieve all the track we made.
tracks: list[Track] = []
@app.get("/tracks")
async def get_tracks():
return tracks
First, we create a track variable with its type as list[Track]
. And a simple get
operation on /track
path that just returns the tracks
list.
This works fine, but we need to store the user input into our tracks
.
@app.post("/tracks")
async def create_track(track: Track):
tracks.append(track)
return track
Add a tracks.append(track)
before our return statement. The append
method is just equivalent to JavaScript’s Array.push()
. Now we should able to create new tracks and temporary store the data inside the tracks
variable.
That all for today. Next time I would like to spend some time on OpenAPI specification… Or maybe not? I’m not sure about it for now. Anyway…
All the source code can be found on my github: https://github.com/LeoCantThinkOfAName/newbie-fastapi/tree/type-data-validation