angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Return only the values of a list of records from FastAPI

By returning only values instead of dictionaries we minimize data transfer size and time.

6 July 2023 Updated 6 July 2023
In API, FastAPI
post main image
https://www.pexels.com/nl-nl/@hellokellybrito/

In Python, everything is a class, which means that model data is similar to a dictionary. But dictionaries have keys. And when you return a list of many dictionaries from FastAPI, the size of the data, keys and values, is usually much more than twice the size of the values. Larger size and more time means that our application is not very efficient, slower than necessary. It also means it consumes more energy, which means it is not very sustainable (sounds good ... ugh).

Below I present and compare two ways to return data from FastAPI:

  • A list of dictionaries
  • A list of tuples containing dictionary values

To make things more exciting, the response consists of a "meta" part and a "data" part. You can try this yourself, the code is below. As always, I am running on Ubuntu 22.04.

The ListResponse class

I already mentioned the ListResponse class in a previous post. What we want to return is two items, 'meta' and 'data'.

    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=count,
            total=total,
        ),
        'data': <list-of-dicts or list-of-tuples>,
    }

The ListResponse class creates a single response object, using the ListMetaResponse class and the data model. To use it:

    response_model=ListResponse(Item)

1. Returning a list of dictionaries

Please refer to the FastAPI document 'Response Model - Return Type' for the model, see links below.
We are using the following model and items:

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []

items = [
    {
        'name': 'Portal Gun', 
        'price': 42.0
    },
    {
        'name': 'Plumbus', 
        'price': 32.0
    },
]

Returning the data is very straightforward; I won't bother you with this here.

2. Returning a list of tuples containing the values

In this case we use a tuple for the values of a dictionary. We cannot use a list because there is no Python Typing support for positional items in a list. The values of the dictionary must be placed at fixed positions in the tuple. This means the model is, see above:

Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]

To get the values in the tuples depends on your application, where does the data come from.  Here I assume the 'items' as shown above, these are 'incomplete' dictionaries.  We can process this to a list of tuples in the following way:

    # extract values from items
    def get_item_values(item):
        d = {
            'name': None,
            'description': None,
            'price': None,
            'tax': None,
            'tags': None,
        } | item
        return tuple(d.values())
    items_values = list(map(get_item_values, items))

If the 'items' come from a database, you can select all fields in the query. The result will be a list of tuples and there is no need for this processing.

Comparing the JSON data

1. List of dictionaries

To get the JSON data, run in another terminal:

curl http://127.0.0.1:8888/items

Result:

{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]}

And the data part only:

[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]

Number of bytes of the data: 148.

2. List of tuples containing the values

To get the JSON data:

curl http://127.0.0.1:8888/items-values

Result:

{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]}

And the data part only:

[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]

Number of bytes of the data: 68.

This is a reduction of 54%!

The code

Below is the code in case you want to try. Create a virtual environment, then:

pip install fastapi
pip install uvicorn

Start the application:

python main.py

To show the items, type in your browser:

http://127.0.0.1:8888/items

To show the items-values, type in your browser:

http://127.0.0.1:8888/items-values

The code:

# main.py
import datetime
from functools import lru_cache
from typing import Any, List, Optional, Tuple, Union

from fastapi import FastAPI, status as fastapi_status
from pydantic import BaseModel, Field, create_model as create_pydantic_model

import uvicorn

# see also:
# https://fastapi.tiangolo.com/tutorial/response-model
class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []

items = [
    {
        'name': 'Portal Gun', 
        'price': 42.0
    },
    {
        'name': 'Plumbus', 
        'price': 32.0
    },
]


class ListMetaResponse(BaseModel):
    page: int = Field(..., title='Pagination page number', example='2')
    per_page: int = Field(..., title='Pagination items per page', example='10')
    count: int = Field(..., title='Number of items returned', example='10')
    total: int = Field(..., title='Total number of items', example='100')


class ListResponse(BaseModel):
    def __init__(self, data_model=None):
        pass

    # KeyError when using the Pydantic model dynamically created by created_model in two Generic Model as response model #3464
    # https://github.com/tiangolo/fastapi/issues/3464
    @lru_cache(None)
    def __new__(cls, data_model=None):
        if hasattr(data_model, '__name__'):
            data_model_name = data_model.__name__
        else:
            data_model_name = '???'
        print(f'data_model_name = {data_model_name}')    
        return create_pydantic_model(
            data_model_name + 'ListResponse',
            meta=(ListMetaResponse, ...),
            data=(List[data_model], ...),
            __base__=BaseModel,
        )


app = FastAPI()


@app.get('/')
def index():
    return 'Hello index'


@app.get(
    '/items', 
    name='Get items',
    status_code=fastapi_status.HTTP_200_OK,
    response_model=ListResponse(Item)
)
async def return_items() -> Any:
    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=len(items),
            total=len(items),
        ),
        'data': items
    }


@app.get(
    '/items-values', 
    name='Get item values',
    status_code=fastapi_status.HTTP_200_OK,
    response_model=ListResponse(Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]),
)
async def return_items_values() -> Any:
    # extract values from items
    def get_item_values(item):
        d = {
            'name': None,
            'description': None,
            'price': None,
            'tax': None,
            'tags': None,
        } | item
        return tuple(d.values())
    items_values = list(map(get_item_values, items))
    print(f'items_values = {items_values}')

    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=len(items),
            total=len(items),
        ),
        'data': items_values,
    }


if __name__ == "__main__":
    uvicorn.run("main:app", host='127.0.0.1', port=8888, reload=True)

Summary

Reducing the amount of data to transfer makes your application more efficient and responsive. Python Typing does not support positional items in a list, which means we use tuples. In many cases, such as selecting data from a database, we don't need an operation to extract the values, because the returned rows are already tuples.

Links / credits

FastAPI - Response Model - Return Type
https://fastapi.tiangolo.com/tutorial/response-model

Pydantic
https://docs.pydantic.dev/latest

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.