Build And Host Fast Data Science Applications Using FastAPI | by Farhad Malik | Towards Data Science

I have recently hosted a number of production-ready data science applications as Restful web services using the FastAPI web framework.
I found FastAPI to be stable and easy to use and for those reasons, I have decided to write an article on FastAPI library outlining the steps we can follow to host a data science application.
FastAPI was released in 2018 and is becoming the de facto choice for building high performant data science applications.
This article will explain what FastAPI is, why it’s superior to its competitors, along with a step by step guide on how to host a data science application using the FastAPI.
Think of FastAPI as a wrapper around a data science application to expose its functionality as a RESTFUL microservice
This article will provide an overview of the following topics:
What Is FastAPI?Key FastAPI FeaturesStep By Step Guide on building and hosting a Python data science application using FastAPI.
What Is FastAPI?
Key FastAPI Features
Step By Step Guide on building and hosting a Python data science application using FastAPI.
This article will build a real-life data science application to demonstrate how to use the FastAPI in your data science project
As the name implies, FastAPI is a high performant web framework. Its performance can be compared with NodeJS and Go and it is rated as one of the fastest Python frameworks available.
Netflix, Uber, Microsoft amongst many other corporations are using the FastAPI library.
FastAPI can help us build APIs with Python 3.6+.
It is based on standard Python type hints. Not only is FastAPI intuitive and straight-forward to use in projects, but the FastAPI code also has 100% test coverage hence it’s production-ready robust code.
Having used Flask for quite some time now, I have noticed that FastAPI is as simple to use as Flask. Matter of fact, FastAPI has adapted its simplicity from the Flask API. Subsequently, in general, I recommend exploring FastAPI for future projects.
FastAPI is built on the Starlette and Pydantic libraries.
The diagram illustrates that the FastAPI uses Starlette for its web parts and Pydantic for the data parts.
Pydantic is a Python library to perform data validation, documentation along with serialisation. Pydantic uses the standard library typing types as defined in PEP 484 to define complex objects.
Let’s explore the features of FastAPI.
The FastAPI is based on the open standards for OpenAPI and JSON Schema. As a result, it can automatically build a SwaggerUI for our web API without any additional coding as long as we use the appropriate Pydantic data types. SwaggerUI is interactive in nature and allows us to test an API directly from the browser. The automatic documentation has been inspired by the APISpec.
I will demonstrate how we can build automatic data model documentation with JSON schema using the FastAPI later in this article.
The FastAPI also lets us validate the input data from the external callers and generates automatic errors to the clients when it receives invalid data. The documentation feature has been inspired by the Django REST framework.
The OpenAPI automatically generates a schema for the application. FastAPI also supports data serialisation and input data parsing which have been inspired by Marshmallow and Webargs. Read more about it here
It’s vital to build a web API that does not block its callers whilst it is performing operations. The asyncio library in Python offers a large number of async capabilities.
The FastAPI library supports Async IO operations. This has been possible due to the fact that the FastAPI is based on the Starlette library.
The Starlette library itself sits on top of the Uvicorn. Uvicorn is a lightning-fast ASGI server that supports asyncio frameworks.
Starlette is a lightweight ASGI framework that enables us to build high-performant asyncio services.
Unlike a WSGI server, ASGI servers can support HTTP/2 and WebSockets. With the help of Uvicorn, we can build a web API that can achieve high throughput in IO-bound contexts.
Starlette uses the newer async IO ASGI server and supports background tasks too. Hence, we can build low latent fast web APIs using the FastAPI
Flask users have to install the Flask-restplus package to create REST endpoints for their data science application.
The FastAPI supports the GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH and TRACE Rest operations without any additional packages. All of the routes along with their operations are automatically generated in the documentation.
I have noticed a pattern in the industry recently. It’s becoming vital to bring a data science solution to the market fast. Plus data science applications are one component of a large ecosystem and they need to interact with a number of services and interfaces. This means it’s extremely important to be able to quickly deploy an application as a web service so that it can be used by the end-users.
The FastAPI is simple to use. It literally takes minutes to host a data science application as a microservice and expose the Rest endpoints that other applications can call. The validation, error handling amongst other API features come out of the box which can help in improving customer satisfaction and reliability on the application.
This brings us to the last section of the article where I will demonstrate the steps to host a data science application.
Let’s start with a real use case to build a Smart data science application.
We are going to build a data science application in Python 3.8The application will scrape online web pages to return a list of companies that are the index constituents of an index, such as FTSE 100, S&P 500 or Dow Jones.The functionality will be exposed via a GET Rest endpoint which we will host on localhost on port 8000. The endpoint will accept a query parameter: index. The index could only be one of the following FTSE 100, S&P 500, or Dow Jones.The endpoint will also ensure that the value is one of the accepted index values. All of the errors will be handled and gracefully sent back to the users/callers of the API.We will use the FastAPI to automatically generate Swagger UI and OpenAPI documentation.
We are going to build a data science application in Python 3.8
The application will scrape online web pages to return a list of companies that are the index constituents of an index, such as FTSE 100, S&P 500 or Dow Jones.
The functionality will be exposed via a GET Rest endpoint which we will host on localhost on port 8000. The endpoint will accept a query parameter: index. The index could only be one of the following FTSE 100, S&P 500, or Dow Jones.
The endpoint will also ensure that the value is one of the accepted index values. All of the errors will be handled and gracefully sent back to the users/callers of the API.
We will use the FastAPI to automatically generate Swagger UI and OpenAPI documentation.
The first step is to create a virtual environment and activate it.
python3 -m venv fintechexplainedfintechexplained\Scripts\activate
The next step is to install the required modules.
Firstly, install the fastapi
pip install fastapi==0.58.1
As a result, it will also install the required dependencies such as starlette==0.13.4 and pydantic==1.5.1.
Then install uvicorn
pip install uvicorn==0.11.5
Now to scrape the web pages, I am going to use the Pandas library. Therefore, the next step is to install pandas:
pip install pandas==1.0.3
That’s the installation step done.
Let's create the folder structure now.
For the sake of simplicity, let’s create the following folder structure.
We have created two folders: SmartApp and src within SmartApp folder. Additionally, let’s create the following three empty Python files:
main.py: This file will contain the code which will start the uvicorn server. It will also contain the code to serve a request and return a response asynchronously. This file will also create an instance of FastAPI() which is the main point of interaction to create the API.models.py: This file will contain the domain classes for the application, such as Company and Configuration.workflow_runner: This file will contain the code that will scrape the web pages and return the company names. This is our data science application code which we are going to expose via FastAPI.
main.py: This file will contain the code which will start the uvicorn server. It will also contain the code to serve a request and return a response asynchronously. This file will also create an instance of FastAPI() which is the main point of interaction to create the API.
models.py: This file will contain the domain classes for the application, such as Company and Configuration.
workflow_runner: This file will contain the code that will scrape the web pages and return the company names. This is our data science application code which we are going to expose via FastAPI.
If you want to create a production-ready solution then I recommend using the cookie-cutter python project. There are many templates available, including one aimed at FastAPI.
Ensure the virtual environment is activated.
The next step involves implementing the Python code.
Open the models.py file and add the following code:
from enum import Enumfrom pydantic import BaseModelclass MyException(Exception):    passclass Company:    def __init__(self, symbol):        self.name = None        self.symbol = symbol        self.sector = None        self.industry = Noneclass Index(str, Enum):    FTSE100 = "FTSE 100"    SNP500 = "S&P 500"    DOWJONE = "Dow Jones"class Configuration(BaseModel):    index: str = None    index_map = {        Index.FTSE100: 'https://en.wikipedia.org/wiki/FTSE_100_Index',        Index.SNP500: 'http://en.wikipedia.org/wiki/List_of_S%26P_500_companies'    }    def get_url(self):        return self.index_map[self.index]
We are going to have a class named Company to represent each constituent of the index.
We are also creating a Configuration class. This class contains the name of the index as an Enum. The enum members will be displayed in the API documentation and in the Swagger UI as a drop down. Note that the Configuration class inherits from the pydantic BaseModel. This will enable us to generate automatic data schema documentation. The configuration class contains a dictionary named index_map. Each index map item has its key as the index enum member and the value as the URL of the webpage which has the companies information.
Also note, I have added a function get_url() that will return the URL for the chosen index.
We have also created a custom Exception class, MyException. We will generate an exception handler for it in workflow_runner.py.
Now that the models have been implemented, let’s add the code to the workflow_runner.py. This is the core of our data science application.
This file will contain the code to perform the following two steps:
For the input index, get its companies by scrapping the appropriate web pageReturn the company names as a list of strings
For the input index, get its companies by scrapping the appropriate web page
Return the company names as a list of strings
Note that the run() method of the workflow_controller has been declared as async to illustrate how we can create a non-blocking async IO operation.
from models import Company, Indeximport pandas as pddef get_snp_500_companies(url):    companies = []    table = pd.read_html(url)[0]    for index, row in table.iterrows():        company = Company(row['Symbol'])        company.name = row['Security']        company.sector = row['GICS Sector']        company.industry = row['GICS Sub Industry']        company.date_added = row['Date first added']        companies.append(company)    return companiesdef get_ftse_companies(url):    companies = []    table = pd.read_html(url)[3]    for index, row in table.iterrows():        company = Company(row['EPIC'])        company.name = row['Company']        company.sector = row['FTSE Industry Classification Benchmark sector[12]']        companies.append(company)    return companiesasync def run(config):    companies_map = {        Index.SNP500: get_snp_500_companies,        Index.FTSE100: get_ftse_companies    }    url = config.get_url()    func_to_get_data = companies_map.get(config.index, None)    if func_to_get_data is None:        raise KeyError(f'{input.index} is not suppported')    companies = func_to_get_data(url)    return [c.name for c in companies]
I am using the pandas.read_html(url) function. It returns all of the tables from the webpage. Therefore, I am selecting the appropriate table from the webpage. As each web page has a different structure, I have registered the index name with the url in the companies map. At runtime, the workflow runner will choose the appropriate url and create instances of the company class by executing the index specific function.
If we select Dow Jone, it will raise an exception. This will help me demonstrate how we can display user-friendly errors to the users.
We are ready to integrate FastAPI now.
Finally, we are going to add the code to the main.py file
First, add the imports:
import uvicornfrom fastapi import FastAPI, Path, Requestfrom fastapi.responses import JSONResponseimport workflow_runnerfrom models import MyException, Configuration, Index
Then create a FastAPI() instance and declare it as the app variable.
We are giving it a title, description, and a version. This information will be automatically displayed in the documentation.
app = FastAPI(title='SMART Data Science Application',              description='A Smart Data Science Application running on FastAPI + uvicorn',              version='0.0.1')
Expose the GET endpoint to handle the request that calls the workflow_runner.run():
@app.get("/{index}")async def get_result(index: Index = Path(..., title="The name of the Index")                     ):    config = Configuration(        index=index    )    try:    result = await workflow_runner.run(config)    return JSONResponse(status_code=200, content=result)except Exception as e:    raise MyException(e)
Notice how I am awaiting on the run function.
Essentially, the @app.get is the REST path operation. It handles the requests with the path “/” and GET operation. It calls the async get_result() function. The parameter index is now a query parameter.
We can also mark query parameters as optional and the documentation will be automatically reflected to accommodate the change. We have declared a title metadata value for the path parameter index. This will appear in the automatic documentation.
We can also add validation rule within the Path() such as gt=0, le=1000 for the integers parameters.
Note, the function get_result() is declared as async. It allows us to serve requests without blocking other calls.
As a result, this feature enables the application to serve requests concurrently.
The last point to note is that all of the exceptions are caught and then MyException is raised.
Although the best practice is to explicitly catch the exceptions and deal with them individually but for the demo purpose, I am catching all exceptions with the wide try/except Exception.
Lastly, for error handling, we define an endpoint that can handle the exceptions of type MyException.
@app.exception_handler(MyException)async def unicorn_exception_handler(request: Request, exc: MyException):    return JSONResponse(        status_code=418,        content={"message": f"Error occurred! Please contact the system admin."},    )
Now, whenever we get an exception of type MyException, it will display the message “Error occurred! Please contact the system admin.”. This has been possible because of the app.exception_handler path. FastAPI also lets us create our own exception handlers.
Finally, add the code to run the uvicorn server.
Note, we are using the port 8000, and the host IP address is localhost (0.0.0.0).
if __name__ == "__main__":    uvicorn.run(app, host="0.0.0.0", port=8000)
We are done with the implementation.
Let’s run the application. Navigate to the folder where main.py is and execute the following line in the terminal:
uvicorn main:app — reload
The main is the name of the python file which we want to run.
The app is the FastAPI object inside the main.py
The reload will enable us to restart the server after code changes which enable fast development during development.
Running the command will display:
←[32mINFO←[0m:     Uvicorn running on ←[1mhttp://127.0.0.1:8000←[0m (Press CTRL+C to quit)←[32mINFO←[0m:     Started reloader process [←[36m←[1m1180←[0m] using ←[36m←[1mstatreload←[0m←[32mINFO←[0m:     Started server process [←[36m7004←[0m]←[32mINFO←[0m:     Waiting for application startup.←[32mINFO←[0m:     Application startup complete.
Uvicorn server has exposed the application on localhost, port 8000.
Plus it has created a Swagger UI for the application
Lastly, it generated OpenAPI documentation for the application.
Firstly, let’s review the automatic API documentation
Navigate to the
http://127.0.0.1:8000/redoc
It will display the Swagger UI.
Clicking on the Index shows the list of indexes:
Running it shows the company names in the response:
Let’s select Dow Jones and see if we get a user-friendly error:
It has been done automatically by FastAPI.
We can call it from Curl:
curl -X GET "http://localhost:8000/FTSE%20100" -H "accept: application/json"
We can open a browser and navigate to the url to view the response:
http://localhost:8000/FTSE%20100
You can download the openAPI specification by navigating to the url:
http://localhost:8000/openapi.json
We can see the validation has been built-in along with the acceptable index values as shown below. The OpenAPI schema powers the Swagger UI. It includes the API and data schema as shown below:
{  "openapi": "3.0.2",  "info": {    "title": "SMART Data Science Application",    "description": "A Smart Data Science Application running on FastAPI + uvicorn",    "version": "0.0.1"  },  "paths": {    "/{index}": {      "get": {        "summary": "Get Result",        "operationId": "get_result__index__get",        "parameters": [          {            "required": true,            "schema": {              "title": "The name of the Index",              "enum": [                "FTSE 100",                "S&P 500",                "Dow Jones"              ],              "type": "string"            },            "name": "index",            "in": "path"          }        ],        "responses": {          "200": {            "description": "Successful Response",            "content": {              "application/json": {                "schema": {}              }            }          },          "422": {            "description": "Validation Error",            "content": {              "application/json": {                "schema": {                  "$ref": "#/components/schemas/HTTPValidationError"                }              }            }          }        }      }    }  },  "components": {    "schemas": {      "HTTPValidationError": {        "title": "HTTPValidationError",        "type": "object",        "properties": {          "detail": {            "title": "Detail",            "type": "array",            "items": {              "$ref": "#/components/schemas/ValidationError"            }          }        }      },      "ValidationError": {        "title": "ValidationError",        "required": [          "loc",          "msg",          "type"        ],        "type": "object",        "properties": {          "loc": {            "title": "Location",            "type": "array",            "items": {              "type": "string"            }          },          "msg": {            "title": "Message",            "type": "string"          },          "type": {            "title": "Error Type",            "type": "string"          }        }      }    }  }}
This is the OpenAPI documentation for the API automatically built by the FastAPI. We can send it to the callers of the API so that they can easily integrate their services with our solution.
Note the index enum members appear in the OpenAPI documentation along with the parameter title and the validation rules.
That’s all it is. If you want to know more about FastAPI, do visit their website for further information.
FastAPI is a high performant web framework. FastAPI was released in 2018 and is becoming the de facto choice for building high performant data science applications.
This article explained what FastAPI is, what its key features are along with a step by step guide on how to use to wrap a data science application into a Restful microservice.