Looking for work! Check my resume here.

· 7 min read

FastAPI Is Not Mature

FastAPI and its ecosystem is touted for more than it is ready for.

I was recommended to give FastAPI a try and so I began using it for my next personal project, Bilolok. At the time, FastAPI was still a bit young and I knew there was some risk adopting a new framework but, hype around the project got my attention and my project was personal so I didn’t worry about the consequencies if I had throw the whole project away and rewrite with Flask later. Plus, I would add some experience with async Python into my repertoire.

At this point async Python has around for awhile but I had not realized how many packages for web development were still not ready for the jump to async. Due to this power vacuum if you will, the Python ecosystem exploded up with many async capable SQL ORM packages and they all looked nice but most of them showed their limits compared to the power of SQLAlchemy when digging into them.

SQLAlchemy

I think SQLAlchemy is an essential tool for its ability to simplify database access for simple use cases and get out of the way for more complex SQL queries. That plus the fact it is heavily recommended in the FastAPI docs is why I write about it here.

Around this time SQLAlchemy had released version 1.4 which introduced async functionaliy and a fundamental shift in thier API for the future of SQLAlchemy on the road to version 2.0. But, SQLAlchemy 1.4 was not stable yet. So I started the Bilolok project with SQLAlchemy 1.3 and ran into issues immediately trying to force the async requests to handle the sync database connections.

The first issue was tearing down SQLAlchemy connections after each request. Something in FastAPI was failing to complete the teardown lifecycle hook which would cause database connections to be left hanging or at best leave unsightly exceptions in the logs.

Also, SQLAlchemy is syncronous while that kinda defeated the purpose of using an asyncronous framework like FastAPI. I like SQLAlchemy and I wanted to use it so rather than compromising on some new ORM that had limited features and unknown bugs, I decided to use a package supported in the FastAPI documentation called “encode/databases” which wrapped SQLAlchemy 1.3 until I was ready for version 1.4 and its native async functionality. encode/databases (which is one of the worst package names) wrapped the SQLAlchemy database connection in some magic that made it async and therefore worked with FastAPI, but that’s just the writing on the box, where in reality it didn’t always work perfectly and it felt like a kludge.

Eventually, SQLAlchemy 1.4 appeared to be stable and I upgraded to that. But now I needed to adapt to its new API and its intricacies with regards to handling relationships which requires a new way to think about using SQLAlchemy. In older versions you could access a model’s relationships without thought at anytime even though that would easily create n+1 problems. Now, you have to carefully plan your SQL queries in advance to have loaded all relationships you will need.

So in the end SQLAlchemy issues I faced are not really a FastAPI maturity problem but in general I found commonly used and powerful packages that are often used for web development just were not ready for async or were working their way there and this means issues will arise when you try to use FastAPI coming from a more mature, sync web framework.

Alembic Revisions

One of the issues upgrading SQLAlchemy meant Alembic became a minor wart in the code base. Alembic does not support async connections to the database so I had to keep an async and sync SQLAlchemy engine in my code base with two different database URIs so Alembic could run syncronous upgrades to my database schema and FastAPI had access to the async engine for normal API requests.

Pydantic

From my background with Flask I had picked up Marshmallow and then later Webargs for handling de/serialization. Especially in my less experienced years, I would abuse the Function and other Marshmallow features that allowed dynamic attributes which would lazily load relationships on the ORM object causing all sorts of n+1 problems if not careful. Marshmallow also allowed methods to handle data differently depending if you were loading or dumping data which added more flexibility but ultimately more complexity to reason about.

The equivelant package recommeneded by the FastAPI documentation is Pydantic which uses the sorta new Python typing features to define a schema’s data types which has a nice, clean API and I was happy to start using it. It also doesn’t differentiate between dumping and loading so you end up creating many more schemas to represent a single entity compared to marshmallow. Which is not too bad and results in explicit, clear schemas. But every schema you need ends up looking like the following.

import uuid
from typing import List, Optional
 
from .base import BaseSchema
 
 
class NakamalSchemaBase(BaseSchema):
    name: Optional[str] = None
    aliases: Optional[List[str]] = None
    lat: Optional[float] = None
    lng: Optional[float] = None
    owner: Optional[str] = None
    phone: Optional[str] = None
    light: Optional[str] = None
    windows: Optional[int] = None
 
 
class NakamalSchemaUpdate(NakamalSchemaBase):
    area_id: Optional[uuid.UUID] = None
    kava_source_id: Optional[uuid.UUID] = None
 
 
class NakamalSchemaIn(NakamalSchemaBase):
    name: str
    light: str
    windows: int
    lat: float
    lng: float
    area_id: uuid.UUID
    kava_source_id: uuid.UUID
 
 
class NakamalSchema(NakamalSchemaBase):
    id: uuid.UUID
    kava_source: NakamalKavaSourceSchema
    resources: List[NakamalResourceSchema]
    area: NakamalAreaSchema
 
 
class NakamalSchemaOut(NakamalSchema):
    pass
 

Overall, the DX with Pydantic is not much different from Marshmallow except, and this many be a big one for some, it doesn’t come with the data validation abilities or serialization hooks built in. Regardless, the transition to using this Pydantic is not as difficult as the work arounds to get SQLAlchemy working with FastAPI.

The Big Issue With Pydantic

Now the reason for this post is I found Pydantic really fell short in more complex relationships. Where Marshmallow and SQLAlchemy handle circular references just fine, Pydantic requires a ForwardRef to handle a bi-directional relationships and any more relationships results in an impossible infinite loop.

Again, this is not per-se a probem with FastAPI but it is the recommended package to use and has no solution after a couple years of people asking how to resolve this. How this issue will affect Tiangolo’s (the guy who created FastAPI) SQLModel project which aims to combine SQLAlchemy and Pydantic models into a single monster model will work for anything beyond toy projects I have not yet seen.

The recommended solution from responses to these issues or comments on StackOverflow seem to be “Don’t have circular dependencies” or the explanation of the ForwardRef solution which only works for most basic two-way relationships. The official solution seems to be have all models in a single file, but for anything beyond a toy project this is not reasonable and I believe shouldn’t be required.

Conclusion

I really don’t think anyone will read this, but just in case I don’t mean to say the work done for FastAPI is bad or not worth merit since it is IMO. I do enjoy FastAPI outside of these and other small nuisances. The ecosystem will improve and in time I believe and these issues will be resolved. But, I just had to vent some frustrations and document this issue for my future self.

Future self here, I found returning relation object IDs as opposed to nested objects alleviated my issue but required I refactor my frontend caching strategy. You can read about it in another post.

But to stay on topic with the title, FastAPI does have a few quirks and I do have more concern for its future when I realize how far the creator is stretching himself between FastAPI, SQLModel and other projects and as far as I know he has still not accepted much community developement support while the issue tracker on Github surpasses 1000.

Performance speed is not my primary concern for my personal projects so my next personal project will likely return to using Flask with some lessons on dependency injection to take with me.

  • python
  • fastapi
Share:
Back to Blog