FastAPI by A Python Beginner (2)

Typing & Data Validation

Leo Lin (leocantthinkofaname)
5 min readOct 5, 2023

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.

FastAPI by A Python Beginner

5 stories
Photo by Cande Westh on Unsplash

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…

track schema

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.

with example body

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

--

--

Leo Lin (leocantthinkofaname)
Leo Lin (leocantthinkofaname)

No responses yet

Write a response