Migrating from Flask to FastAPI, Part 1¶
At Forethought we mainly use Python to develop all the product components and services. It's the perfect language for us as we use complex machine learning, data processing, APIs, and others. Python works great for those use cases, and having a common programming language facilitates the development and interaction between all the teams.
Up to a year ago, most of the APIs and backend services were built with Flask, it worked great for us and allowed us to keep growing the products. However, for our particular use cases, mainly building APIs to handle data, it made sense to migrate to FastAPI to get the extra benefits like improved developer productivity, increased code quality, correctness and certainty, automatic data validation, serialization, and documentation, etc.
Nevertheless, we have a huge codebase, the products are constantly evolving. We have many PRs merged daily from the dozens of engineers across all teams, and we release multiple times a day.
In many cases we have to accomodate the code and systems to fit our customer's needs and all the integrations we support, we need to be able to communicate with different APIs, support different authentication methods for our APIs, support backward compatibility, etc. This also means that the code inherently requires some complexity and advanced patterns.
All this makes a migration challenging, and a moving target as well. But now that we have successfully migrated to FastAPI for around a year and we can see the benefits, we want to share the different techniques we used, things we learned, and different tips and tricks that could be useful to you.
This blog series is made of 3 parts, you're reading the first, preparing the ground for the actual migration.
One of the main objectives we had was to keep compatibility as much as possible. We didn't want to change all the code, we wanted to be able to preserve and re-use as much of our code base as possible.
The focus was on migrating the API from Flask to FastAPI, doing everything to keep the rest of the code just working, to make it easier for the other teams to continue their work undisrupted, and to minimize the surface of points the migration would touch.
This post is by no means a comparison of Flask vs. FastAPI. It's written by the same author of FastAPI (working at Forethought 🤓), so a comparison would be obviously biased.
We are grateful for all what Flask has provided us, and it just made sense for us to switch to FastAPI. It doesn't mean it would necessarily make sense for you to switch.
This post is not trying to convince you to migrate from Flask to FastAPI, I won't show FastAPI features or possible advantages extensively.
If you are still deciding if it would make sense to migrate from Flask to FastAPI, you can probably spend 10 or 20 minutes following the first tutorial on the first page of the FastAPI docs. Or you can also read the FastAPI docs for Alternatives, Inspiration and Comparisons.
Here I'm assuming you already want to migrate to FastAPI, so I will show you the code changes needed, caveats, and how to solve them. If you are migrating to FastAPI, you might learn a thing or two from this post to help you in your journey.
There are probably two main strategies or approaches to migrate from Flask to FastAPI. Let's see them quickly.
Migrate All in One Go¶
One approach is to migrate everything (or most of everything) in one go, this is the simplest and most obvious way to approach it. It's probably also more risky if you are not sure about what you will get and if it's worth it, as you have to work a lot before trying everything for the first time, and you could also find some caveats that you could have not known beforehand.
In this case, you would have one specific moment where you finish the migration, stop using Flask, and start using FastAPI, in one go.
The second approach would be to migrate gradually.
You could add a layer on top of your backend to redirect to internal microservices based on, for example, an API path prefix. For this you could use a front end proxy, a tool like Traefik.
Then you could run a Flask and FastAPI applications side by side, for example each one in its own container. And then every request that went to
/api/v1 would be directed to the Flask application, and every request that went to
/api/v2 would be directed to the FastAPI application.
This way you could have all your API writen in two frameworks under the same domain.
Any new endpoint that you write could be done in FastAPI under
/api/v2 while the old endpoints are still served by Flask. And you could also gradually move the old endpoints in Flask under
/api/v1 to the new
/api/v2 with FastAPI. This way you could even serve both versions at the same time.
This approach could be considered less risky if you are not sure what you will get out of it and if you are not sure yet if migrating to FastAPI is the best choice for your product. It would also require some extra effort setting up the frontend proxy.
For this strategy to work, you would probably also need to control all the clients, for example, it's a frontend web app that you can just update and re-deploy as you need. There are no possibly outdated clients. This is what would allow you to point them to the new API prefix
/api/v2, possibly updating the payloads or responses as needed, if you decided to change them, etc.
The main benefit of this approach would be that you would be able to decide what sections of your API are migrated, for example only migrating first low-traffic or less important ones.
In the case of Forethought, we knew the benefits of FastAPI and were sure that we wanted to migrate everything. It also made some things easier. So we took the route of migrating all (or almost all) in one go.
Most of the ideas here will probably be useful to you either way. And I'll also show you how to handle compatibility with the code while having both a Flask and FastAPI app at the same time.
Prepare the Current Code Base¶
Before actually doing any migration to FastAPI, it would be beneficial to perform some steps to make sure the code is stable and clean. This will simplify things down the line.
It's probably a good idea to collect errors from production if you are not doing it already. You could try out Sentry or another provider.
However you do it, it's certainly a good idea to be able to see those errors from production.
Hopefully you are testing your application already. Tests give you confidence that your application is doing what it should, but sometimes, more importantly, they give you confidence that new changes and refactors are not affecting things that were already working.
If you have tests for your API, you will be able to re-use most of that same code during the migration and check that the new FastAPI code (migrated from Flask) is still working as intended.
You will probably have to adjust and tweak the tests a bit, but you will conciously see if the little changes and tweaks make sense, if it's just a change related to testing or if it's a change that affects your API clients.
In most of the cases you will be able to keep the exposed API and the communication with clients intact (which is what we did), but there might be cases where the client communication should actually be improved, and you might decide to also update the client in those small cases as part of the migration.
No Tests or Failing Tests¶
If you didn't have tests, or if your tests are currently failing, this means that, for a while, new changes to your API could have been breaking your application. In this case you could have a feeling that things are mostly working, but you would probably not have full certainty of what is working, what isn't, what recent changes broke some parts, etc.
In this case, you would have the option of adding or fixing the current tests. But this might be costly, especially knowing that you are migrating the code base that you are testing.
The other option would be to just migrate to FastAPI and manually check if things seem to be working, the same as you have been doing. This would be faster, but riskier. Nevertheless it might be an acceptable option as you are probably migrating to FastAPI to be able to improve development efficiency and improve the certainty that your code is free from bugs detectable by types and tools like mypy. And as you will also get automatic data validation, serialization, and documentation, that could be a range of features you don't have to test as much as if you didn't have those features by default. So, there's a chance that if you worked a lot on adding many types of new tests before the migration, for example to test custom data validation, you might just end up removing those tests after the migration to FastAPI.
If you have passing tests and they have a good coverage of your code base, you would be in the ideal situation, as it will make the migration easier and you will have much better certainty that the new implementation is correct. Fortunately this was our case.
There are several tools that you can use to format your code. This is probably a good step before migrating to FastAPI as it will simplify things down the line.
It could be easy to consider code formatting as a purely stylistic and aesthetic feature. But it actually helps a lot with the structure of the code. And in particular, if you use a formatter like Black, it will minimize the diff in code changes. This means that when your code is formatted with Black and then you make a change (for example in a pull request), the number of lines of code and characters changed will be the minimum. This is particularly useful if you are migrating a large part of the code base, as it will allow reviewers to focus on the changes.
The recommendation would be to format the whole code base in a single PR, independent of any other change. It would be a huge PR, changing lots of files. But after that, any future PR will have the minimal changes and will have consistent structure.
It's probably a bad idea to add PRs changing internal parts of the code and formatting at the same time. The code changes will seem to be a lot, and it would be easy to get confused and miss actual logic changes for having them intermixed with pure formatting changes.
As Black doesn't change the structure of the code base, only the format, it should be relatively safe to use. It's also the most common formatter, it's pretty much the standard formatter for Python right now, so you can have some certainty it probably won't break things easily.
Make sure to do this on a single PR, test everything, and make a release with just these changes, to be sure everything is working as expected.
The reason to keep these changes isolated from others is that formatting with Black will probably change a lot of code (lines, characters), but you can have a very high confidence that it won't affect the behavior in your code.
If you mixed Black formatting, with say, another formatter that can have some caveats or behavior changes, all in the same PR, and then you realized that the second formatter broke something, you would have to try and find what was the change among all the changes from Black.
But if you had the second formatter's changes isolated in a single PR, it would be much easier to see what changed and fix it.
You can also add Black to CI to check that new code still conforms to the format and code style.
After formatting with Black, you might want to use Isort to sort the imports consistently. You would probably use the "Black profile" to keep it in line with Black.
This again will help keeping the code consistent and minimizing diffs in future changes.
The effects of applying Isort would probably be minimal. But it could still affect how the code behaves in corner cases. As this will change the order in which things are imported, if you have side effects at the top level of your modules (files), if any of those side effects depends on the order in which they happen, this could affect that. Again, it's a very strange corner case, but depending on how you structure your code, it could affect you.
It would be a good idea to make Isort changes isolated from Black formatting or any code logic changes. Maybe in its own PR, or in multiple PRs. Then test everything and make a release with just that, to make sure it didn't break anything in your code base, mainly because the changes from Isort are much less than those of Black, but they have a slightly higher chance of breaking some behavior.
You might also want to try out Ruff for sorting imports.
You might also want to add Isort (or Ruff) to CI>
Next, you could format your code with Autoflake. It can be configured to remove unused variables, unused imports, and other things.
Autoflake applies some formatting and automatic fixes based on Flake8. After you finish the FastAPI migration, you would probably benefit from using Flake8, but for now, let's just focus on the steps before migrating to FastAPI.
This can help a lot cleaning your code up and finding unnecessary code sections. The number of changes from Autoflake is normally fewer than Black and normally fewer than Isort. But as this can remove sections of code (unused variables, unused imports), and those could have side effects with behaviors that you depend on, the chance of this accidentally breaking something is higher than Isort and Black.
You will most certainly want to do this isolated from Black and Isort, maybe in a single PR or in multiple PRs, but without any other code logic changes.
You might also want to try out Ruff for removing unused imports.
Autoflake with Flask¶
Here's an important caveat for Flask. Depending on your setup and code structure, there's a chance that you create the
app object in one module (file), and then you import that in other modules. And then you import those modules back to the main file, but below the line where you create the
app object, to reflect those changes in the
app object created on the main module.
In this case, you are not importing those secondary modules to use them, but only for their side effects, they add routes to your Flask
Autoflake will notice these "unused imports" and will remove them. Make sure you pay special attention to that. Removing those imports would mean removing whole sections of your API.
To prevent Flake8 from complaining that the imports are not used, and that way prevent Autoflake from removing them, you can add
assert statements for the modules themselves.
for example, it could look like this:
from flask import Flask app = Flask(__name__) from . import users, items assert users # to ensure this module is imported and not removed assert items # to ensure this module is imported and not removed
You probably guessed it already, this was my first (and only, up to now 🤞) p0 incident at Forethought. 😅 🔥
Containers and (Micro)services¶
There's a chance that you have multiple systems, maybe you even have them already separated in different (micro)services. But it's possible that you have them all together in the same code base and maybe even all mixed in the same Flask app.
There could be situations where you are using a plugin for Flask to handle some particular use case and you don't want to migrate it, maybe it's a special feature and you don't need to change much there or add many features, or maybe it's already deprecated but you don't want to remove it just yet.
In cases like that, it could make sense to, for example, create separated Docker images/containers/deployments for the small Flask service and leave the rest apart. It could be beneficial to separate these things early, even before migrating to FastAPI, to have those uncertainties already handled.
There are probably gonna be cases where you need to decouple the Flask code from your internal logic, to be able to share those internals in different places. But at least splitting parts of the code that work independently might make sense.
Later when you migrate the main API to FastAPI, you can keep Flask in these services as they are, and keep changing the new FastAPI code without having to worry too much about the old one that is no longer being modified and where the current structure with Flask is enough.
This should give you a quick overview of the main strategies to migrate from Flask to FastAPI, and how to prepare the ground for the actual migration coming next.