How we automated our FastAPI migration at Partoo

Julien Danh
partoo
Published in
8 min readNov 14, 2023

Introduction

In the fast-moving world of software, keeping up with new technologies is a must. Our company used the Pyramid framework for building web applications for a long time. However, Pyramid started to fall behind, missing modern Python features like asynchronous programming. With the upcoming Python 3.12, we saw warning signs of future compatibility issues. This led us to look for other options, and we found FastAPI. In this article, I’ll discuss why we moved away from Pyramid, and how we switched to FastAPI in less than 2 months, embracing a framework better suited for today’s Python ecosystem. Through this journey, I hope to offer useful insights to those considering a similar change.

Preliminary Considerations

  • Evaluating the Need for Migration: The first question we faced was whether a migration to FastAPI was truly necessary. It’s important to consider the effort and resources involved in such a transition.
  • Current Framework Viability: In scenarios where you’re working with a modern and well-maintained framework like Flask or Django, shifting to FastAPI might not be worth the hassle. It’s essential to weigh the benefits against the costs and complexities involved.
  • Dealing with Aging Technologies: On the other hand, if your current technology is on the decline or not keeping pace with modern advancements, considering a migration to a more robust and future-proof framework like FastAPI becomes more relevant.

Issues with Pyramid

  • Hard-to-use documentation: Even after years of using Pyramid, our team struggled with its complex documentation. Only a few could understand it well, making it tough for both new and existing team members.
  • Missing Modern python features: We wanted to use modern Python features but Pyramid didn’t support key ones like asynchronous programming and ASGI. This held us back as these features are becoming important in web development today.
  • Stopped development: A big concern was Pyramid’s halted development. It wasn’t just lacking in features but also in maintenance, making us worry about its future use, especially with Python 3.12 coming up.
  • Performances issues on high load: With an increasing user base, we started facing performance issues during high traffic times on Pyramid. The framework struggled to manage the load efficiently, leading to slower response times and, occasionally, server timeouts. This wasn’t just affecting our user experience but also was signaling a need for a more robust and scalable framework capable of handling high traffic effortlessly.

Why FastAPI ?

  • Overcoming Pyramid’s Shortcomings: FastAPI resolves the major problems we encountered with Pyramid, offering strong support for modern Python features like asynchronous programming and ASGI, aligning us with industry advancements.
  • Gradual Migration Capability: A standout feature of FastAPI is its support for progressive migration. This was vital given our nearly decade-long codebase in Pyramid. It allows us to transition one service at a time, minimizing risks and ensuring uninterrupted business operations during the migration phase. (Mount any WSGI application in FastAPI: https://fastapi.tiangolo.com/advanced/wsgi/#including-wsgi-flask-django-others)
  • Excellent Documentation: FastAPI boasts clear and intuitive documentation, aiding our transition significantly. The well-organized guides and examples provided a stark contrast to our previous framework, enabling swift onboarding for new team members and facilitating a smoother migration process. The documentation played a pivotal role in accelerating our development efforts as we transitioned.

Proof of Concept

FastAPI offers the possibility to mount any WSGI app inside it (similar to importing a router, but with a whole application)

POC with one endpoint migrated

Before fully committing to FastAPI, it was essential for us to conduct a proof of concept. This would allow us to validate FastAPI’s capabilities and evaluate its performance against our existing Pyramid setup.

  • Objective: Aim was simple: run our large app with 399 endpoints on Pyramid and one endpoint on FastAPI, for a real-world comparison.
  • Process: Setting up the proof of concept took under a day due to FastAPI’s simplicity.
  • Running the Dual Setup: Configured our app to have 399 Pyramid endpoints and rerouted one to FastAPI, allowing a head-to-head framework comparison.
  • Performance Benchmarking: We benchmarked the FastAPI endpoint against its Pyramid counterpart, revealing a significant performance gain of 33% for FastAPI, mainly under high load. The tests, conducted using Locust with 50 concurrent users, affirmed FastAPI’s superior performance and compatibility with our existing setup.
Result of benchmark on one migrated endpoint

Building a Strategy

Successfully migrating from Pyramid to FastAPI required a well-crafted strategy, particularly due to the scale and complexities involved. Here’s how we approached it:

  • Basic Strategy: Our approach involved mounting our existing Pyramid application (WSGI) within FastAPI (ASGI). Instead of running a gunicorn worker we started using uvicorn (It is still doable to use gunicorn with uvicorn workers if you need the process management part).
  • Solid Testing Framework: . Multiple layers of tests — integration, end-to-end, and manual testing — will minimize bug risks during migration. We updated our integration tests to use FastAPI’s test client (https://www.starlette.io/testclient/), modifying only the test client, not the tests, ensuring a consistent comparison and verifying no functionality was lost in the transition.

Addressing Code Coupling:

Our code was tightly linked with Pyramid, giving us two paths:

  • Framework-Neutral Rewrite: This approach aimed to restructure our codebase to be completely independent from any specific framework, making it universally adaptable (Clean code / DDD). Although ideal, it was deemed cost-prohibitive due to the extensive modifications required to disentangle our code from Pyramid’s specifications. This method would have provided the highest level of flexibility and future-proofing, but the associated costs and resource demands steered us towards seeking a more pragmatic solution.
  • Adapter Pattern: We chose the Adapter Pattern to bridge the gap between Pyramid and Starlette requests, offering a cost-effective solution for compatibility between the two frameworks. However, this adapter represents a form of technical debt. The plan is to gradually rework the code to native FastAPI over time, improving sections of the codebase incrementally as the team works on related areas, thereby phasing out the adapter and reducing the technical debt.

Implementing the Adapter:

  • Enumerate all methods/properties on your request object, crafting adaptations for Starlette to Pyramid compatibility.
  • Expect a tedious process, with numerous iterations likely needed to fine-tune the adapter based on test case responses.
  • Good code coverage will be instrumental in quickly identifying issues.

Simplified example of an endpoint migration

A simple example of migration from Pyramid’s @view_config decorator to FastAPI's @router.get decorator, showcasing a shift in handling route definitions and role requirements.

### Pyramid
@view_config(route_name="some_route", renderer="json", required_role="ADMIN")
def my_controller(request: Request):
"""some docstring"""
user = request.current_user
# Some code using request

return {"key": "value"}

### Fast API
router= APIRouter()

@router.get("/some_route")
def controller_required_role(
request: RequestAdapter,
authentication_service: AuthenticationService,
user: UserDependency
):
"""docstring"""
authentication_service.assert_user_has_role("admin")

# Some code using request

return {"key": "value"}

Modelisation of the migration process

  • Atomic Refactoring: Following Martin Fowler’s catalog, decomposing the migration into logical steps aids in automation, scaling the process efficiently. While these steps might not always result in immediately functional code, they provide a clear framework for structuring the automated refactoring process.

The above example can be modelled by the following diagram and each step can be automated

Automate the refactoring

  • Choose the right tool: We needed a tool that can handle complex refactoring, based on how the code is written and even using annotation. Another key point was to not completely mess the code, you can use the standard python AST but it will destroy your formatting and comments.We decided to use libCST a Concrete syntax tree framework opensourced by meta: https://github.com/Instagram/LibCST
  • Don’t automate everything: Don’t try automate everything for the sake of automation. It is easy to fall into creating an automation for 6 hours to solve a problem that can be done in 20minutes.
  • Automate the big wins: You will some scenarios that just happen in 95%+ of your endpoint. This is where you get high value of automation ! Spending a day to automate a task that must be repeated 400 times is insane amount of time won ! Do it !

Example of automation for a step

Let’s write a libCST transformer for a simple step (our real script was more complex, but for the sake of the example we are going simplify it)

We focus on the step “Replace user assignation by User dependency” which is basically getting the user from a FastAPI dependency instead of a request attribute. (Note that it could be written directly using libcst codemod)

Handling the delivery

Context

  • Impact Teams: Entrusted with feature development, spanning 10 teams distributed across 3 distinct tribes.
  • Core Tribe: Focuses on transversal topics, with an architect team dedicated to handling the migration, composed of 2 backend engineers at the time of migration.

The delivery

We spent a few weeks building the libCST transformers along with the adapters. With these in place, we were able to auto-generate close to 150 pull requests, smartly divided based on the scopes of different teams.

This was no small feat — we were looking at migrating 350,000 lines of code and just under 400 controllers. Although we had a powerful automation setup, getting all these changes live took us about a month. This time included sorting out some unique issues that our automated tools couldn’t handle.

The success of this migration hinged on syncing well with all the feature teams. Since we were all interdependent for this project, having everyone on the same page was crucial. The code for migration was all set; we just needed the go-ahead from each team and a thumbs up from Quality Assurance (QA).

To keep tabs on everything, we used a Notion table. This helped us know who was doing what and how far along they were, especially as some teams had more time on their hands than others during August.

This experience showed us that with a good mix of the right tools like libCST, well-planned automation, and solid teamwork, managing a hefty code migration becomes a lot more doable.

--

--