Build powerful APIs with Python’s FastAPI
Hi folks,
In this piece I would like to introduce one relatively new Python’s framework for building simple and powerful web framework for building web apps, APIs, etc. in Python 3.7+ based on type hints.
What is a type hint?
First of all, python is not statically typed language as you may know and as such there isn’t a compiler that could check types of arguments and variables that are passed around. For example
>>> def add(a, b):
>>> return a + b
>>>
>>> add(1,2)
3
>>> add('abc', 'def')
abcdefWhenever you call the add function it will naively try to add the elements regardless of our type. We might have meant to pass only integer types but that is not know by the interpreter until the program is started. Once you run it, the arguments get evaluated during the runtime and in case of int they get summed, in case string — they get concatenated.
Type hints make possible for us to annotate what is should be type of our arguments like so.
>>> def add(a: int, b: int):
>>> return a + b
>>>
>>> add('abc', 'def')
abcdefUnfortunately for us, type annotations does not change anything and we still can pass strings to our function. If it was a statically typed language, we would get an error during compilation complaining that the wrong type is passed.
If annotations do nothing, why we need them you may ask? Well they are not completely useless because thanks to them tools such as mypy (static type checker for Python) could do a static type checking in order to verify the what is being passed to our functions and classes.
Annotations live in the objects dictionary which could be checked with dir
>>> dir(add)
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> add.__annotations__
{'a': <class 'int'>, 'b': <class 'int'>}
>>>To learn why type hints/annotations are important, please go ahead to FastAPI’s official docs to read about it.
Let’s now focus on building something small and fun.
One small project that you could start with is a simple API that accepts GET and POST requests. We would use the POST request to upload data to a SQL database and GET to retrieve the values.
Building a development database using SQLite3
# create_db.py
import sqlite3
con = sqlite3.connect('numbers.db')
cur = con.cursor()
cur.execute("CREATE TABLE numbers(team VARCHAR, counter INT);")
con.close()
print('Database created successfully!')SQLite3 has a simple API allowing us to create development database in a few short lines. We first create the database using connect (it connects to a database if present, or creates it and connects if it is not present) and get a database cursor (more about db cursors). We then create a simple table called numbers with two columns — team and counter. Once done, we close the connection and print a help message to our users.
Build a POST route for DB updates
We now move to the actual POST API route using FastAPI.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.post('/postNumber')
async def post_number(number: Number):
print(number)The first thing that we encounter as something our of ordinary is the number: Number type annotation. On this line, we declare that our POST API route will accept a number argument that is of a class Number. But we still haven’t created such. Lets do that.
# main.py
from pydantic import BaseModel
class Number(BaseModel):
team: str
number: intOne new thing here — the pydantic BaseModel. Pydantic is data validation and settings management using Python type annotations. Pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid. FastAPI heavily relies on pydantic for those checks and that’s what makes it so great tool for development.
Our Number class states that only two arguments are allowed on our POST route — team of type string and number of type integer
Lets test our API. Start the server using uvicorn where main is the name of our main.py file, while app is the instance of our FastAPI that we’ve declared in main.py
uvicorn main:app --reload
INFO: Will watch for changes in these directories: ['C:\\Users\\I548142\\Documents\\cloudops-tools\\metrics-collector\\new']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12196] using WatchFiles
INFO: Started server process [28740]
INFO: Waiting for application startup.
INFO: Application startup complete.A simple POST request with cURL will show what we’ve done so far.
curl --location --request POST '127.0.0.1:8000/postNumber' --header 'Content-Type: application/json' --data-raw '{"team": "Medium", "number": 1}'
[ "Data inserted successfully" ]On the server side, we’ve received the following data.
team='Medium' number=1
INFO: 127.0.0.1:58017 - "POST /postNumber HTTP/1.1" 200 OKThis means that we’ve successfully passed JSON data from a user to our back-end. Now we could actually push the data to SQLite3 as so.
# main.py (updated)
@app.post('/postNumber')
async def post_number(number: Number):
cursor = con.cursor()
cursor.execute("INSERT INTO numbers (team, counter) VALUES (?, ?)", (number.team, number.number))
con.commit()
return {'Data inserted successfully'}We again get a cursor to the database and execute a INSERT statement into the numbers table with our parameters from the previous cURL query.
Now, we come to the part where we need to retrieve data from our database and verify if we’ve actually submitted anything to it. For the purpose of this, we will build a simple GET route and retrieve data for all teams.
# main.py (updated)
@app.get('/getNumbers')
async def get_numbers():
cursor = con.cursor()
cursor.execute("SELECT * from numbers")
return cursor.fetchall()The GET route is pretty straight forward. We obtain cursor to the database and execute SELECT * to retrieve all data from numbers table. We then fetch that data using fetchall() API and return to the user.
curl --location --request GET '127.0.0.1:8000/getNumbers'
[ ["Medium",1] ]The returned data is broken down into individual rows as if they were rows from the database. That means that if we had another row for example we would received another list like so:
# Add new row
curl --location --request POST '127.0.0.1:8000/postNumber' --header 'Content-Type: application/json' --data-raw '{"team": "Google", "number": 1}'
[ "Data inserted successfully" ]
# Retrieve data
curl --location --request GET '127.0.0.1:8000/getNumbers'
[
["Medium",1],
["Google",1]
]With this we conclude this article. Hope you learnt something new today. The full source code is down below.
# create_db.py
import sqlite3
con = sqlite3.connect('numbers.db')
cur = con.cursor()
cur.execute("CREATE TABLE numbers(team VARCHAR, counter INT);")
con.close()
print('Database created successfully!')# main.py
from fastapi import FastAPI
from pydantic import BaseModel
import sqlite3
con = sqlite3.connect('numbers.db')
class Number(BaseModel):
team: str
number: int
app = FastAPI()
@app.post('/postNumber')
async def post_number(number: Number):
cursor = con.cursor()
cursor.execute("INSERT INTO numbers (team, counter) VALUES (?, ?)", (number.team, number.number))
con.commit()
return {'Data inserted successfully'}
@app.get('/getNumbers')
async def get_numbers():
cursor = con.cursor()
cursor.execute("SELECT * from numbers")
data = cursor.fetchall()
return data





