avatarJordan P. Raychev

Summary

The provided content introduces Python's FastAPI framework for building web applications and APIs, emphasizing the use of type hints for static type checking with tools like mypy, and demonstrates how to create a simple API with SQLite3 for database operations.

Abstract

The article "Build powerful APIs with Python’s FastAPI" by an unnamed author on the undefined website delves into the capabilities of FastAPI, a modern Python web framework designed for creating APIs and web applications. The author explains the concept of type hints in Python, which, despite not enforcing type checking at runtime, enable static type checking when used with tools such as mypy. The article guides readers through setting up a development database using SQLite3 and illustrates how to build a POST route for database updates and a GET route for data retrieval within a FastAPI application. The author also introduces pydantic's BaseModel for data validation and demonstrates the practical use of FastAPI with a simple example that includes inserting and fetching data from the database. The article concludes with the full source code for the example application, aiming to provide a hands-on learning experience for developers interested in FastAPI.

Opinions

  • The author expresses that type hints, although not enforced at runtime in Python, are valuable for static type checking with tools like mypy.
  • FastAPI is praised for its ability to leverage Python type annotations to create powerful and simple web frameworks.
  • Pydantic is highlighted as a useful library for data validation and settings management in conjunction with FastAPI, enforcing type hints at runtime and providing user-friendly error messages.
  • The author suggests that FastAPI's reliance on pydantic for data validation makes it a superior tool for backend development.
  • The article conveys enthusiasm for FastAPI's potential to streamline the development of web APIs and applications, particularly for those familiar with Python 3.7+.

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.

Photo by UX Store on Unsplash

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')
abcdef

Whenever 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')
abcdef

Unfortunately 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: int

One 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 OK

This 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
Python
Fastapi
API
Sql
Programming
Recommended from ReadMedium