If you are looking to create a web app in Python, choosing a framework will be the first technical decision you will need to make. With so many options, which one should you go for?
Most web apps these days are REST applications that perform CRUD operations on your selected database. This means that a lot of the code you want to write is quite generic and could be simplified a lot with generic code from libraries.
Specifically, we are looking for two types of libraries:
- A Web framework
- A Database framework
Because we are making a new web app, we have the freedom to look to anything, from old and reliable to new and fancy. We will evaluate on the following criteria:
- It should be simple and extensible
- It should be performant
- It should minimize the amount of work we have to do
- It should use modern Python
- If possible the Web and Database library should work together nicely
Lastly, there is always the X-factor, the library should work in the way we like. In the end, most framework and libraries will get the job done so we should also like using the framework.
For the reader short on time, there is a comparison table at the end of the article. And the last paragraph on each section is also a summary of when to use each library.
Django is one of the biggest and oldest web frameworks and is “batteries included”. This means that it provides almost anything you need for a web framework.
It has class- and function-based views, it supports creating template pages very well, it has an excellent admin site in which you can easily connect to your database without messing around in SQL.
Additionally, being old and well-established, it has a ton of libraries that are built on top of it. From small things like CORS headers to larger things like celery integration and scheduling. However, the best one is Django Rest Framework, which is a library that has a REST-first approach to Django and provides shortcuts to validation, generic classes and automatic model GET (all and one item), PUT, PATCH, DELETE and even HEAD integration. This makes it the right way to use Django these days, in my opinion.
However, not everything pulls in Django’s favour. One of the most common critiques is that Django is very opinioned and tells you how to manage your code. While I personally don’t really follow in that train of thought, for small web applications it could be a bit overkill. Secondly, Django is slow. And lastly, a personal negative for me is that Django feels old. It does not use many modern Python features. Typing support is lacking (looking at your QuerySets), async is not included yet and Django has a tendency to make duplicate normal Python things (to be fair, they were not included when Django made them).
To me, Django is a very solid choice for any big project with a focus on reliability and for very small CRUD projects where you don’t want to bother with a big database setup.
Flask is the most popular micro web framework out there. It is light-weight, easy to start with, and does not tell you how to code your way. In a way, it is the opposite of Django.
Flask is generally used with function-based views, although it also supports class-based ones. It supports Jinja natively. It does not provide a database ORM, leaving you free to choose whatever you please.
As one of the most popular frameworks, it also has a ton of libraries including integrations with SQLAlchemy, automatic validations, authentication and security middleware etc. As it is a pure web framework, it makes it easier to follow a good REST design.
That does not mean there are no downsides. As a micro web framework, it only provides barebones HTTP handling out of the box. While that might be a benefit, for creating REST endpoints it means you have to add a lot of things manually such as validation and documentation. Secondly, it suffers from some of the same problems as Django. It is quite slow and, again in my opinion, just feels old. It uses barely any of the modern Python features. While its libraries make up for some of its flaws, even most of the libraries are old (but tested and validated).
Flask is a solid choice for small to medium-sized web-focused apps for which easy of use and useful libraries are the primary concerns.
Sanic is a newer asynchronous web framework that focuses on both technical speed as development speed. While their old logo might not have given a lot of trust, it is one of the better async frameworks out there.
From a development point of view, it resembles Flask very closely. It promotes function-based views but also supports class-based views. Request parameters etc are found in the function requests.
For speed, its namesake does not lie and it provides the highest speed of the frameworks considered here. Together with an async way of working, it will help you a lot to scale up easily. As an up and coming framework, it has some libraries covering the most common use cases, although not nearly as much as Django or Flask.
On the other side of the coin, it suffers from similar drawbacks as Flask. Its focus on micro requires extra overhead for nice REST endpoints with validation etc. And as a smaller community, it won’t have the same support as Flask would have.
Choose Sanic if you need a highly performant web application, or if you want an asynchronous version of Flask.
Lastly, for the web frameworks, we have FastAPI. FastAPI is a newer asynchronous web framework that focuses on integrating typing into the REST code base. It’s a combination of Starlette and Pydantic into a web framework.
The code style is similar to Flask with function-based views. The Pydantic models automatically validate the requests and provide the correct error responses to the user if necessary. All of this is created in typed Python which allows you to pick up errors in your IDE and with mypy.
As its name suggests, it’s also fast, just a tad bit slower as Sanic. It works both async and sync depending on your code style. With the Pydanctic models, it also auto-generates a Swagger and Redoc documentation. As a new framework, its integration list is not huge but it has some very useful integrations, including SQLAlchemy and Tortoise ORM.
FastAPI shines when you are looking for a web framework where input validation will be an important part, and you are looking to improve your code quality with typing.
Django also has an Object-Relationship Mapper (ORM), which maps your code model to a database model. As part of the web framework, of course, it integrates flawlessly into the rest of the framework.
Working with the Django ORM is very easy. Defining classes is straight forward. Just define something as a database model and say which fields it needs to have. It supports straight forward fields and also Foreign Keys, Many to Many relationships and even One-to-Ones. Using models is also very easy. Reverse direct keys are imported into the other model so can be accessed directly, and it all works very smoothly.
Additionally, one of my favourite parts is the migration framework that automatically creates the logic to create and adjust your SQL tables. While the out of the box solution is not always perfect, its guesses are right more often than not and you can always manually adjust the updates itself.
On the downside, the Django ORM brings along all of Django with it. Using just the ORM part seems like a bit of overkill as you will still need to integrate Django as a whole. There are also quite some quirks to avoid querying your data very often. And like Django, it’s starting to feel quite old and does not support typing or async.
Use the Django ORM when you are using Django as your web framework. For other cases, I wouldn’t immediately recommend it, even though I enjoyed working with it very much.
SQLAlchemy is the biggest ORM system in Python, and for good reason. It is extensive, will cover just about any use case and supports all databases under the sun.
It is split up into a core part and the ORM part, which helps with connecting to any database you want to support and setting up the tables. The ORM is where the magic happens, helping you avoid writing delicate SQL queries. In my opinion, the way of thinking resembles SQL logic, manually joining tables. This allows you to optimise your queries very well.
While migrations are not natively supported, the Alembic library builds this in on top of SQLAlchemy. It can also auto-generate the migrations for you and will guess more often than not the correct way of moving between database states. Lastly, as the biggest ORM library, there are a ton of libraries build on top of it, helping you with a lot of use cases.
On the negative side, it is complicated to work with. Its SQL like style doesn't flow as neatly into the Python style. Because of this, the learning curve is higher for SQLAlchemy than for example for Django. Typing is not natively present (although it does have a mypy stub) and async is currently a beta feature.
If you’re looking for a solid ORM that many people are familiar with, and will be able to handle any edge case you throw at it, go for SQLAlchemy.
Tortoise ORM is a new async ORM library inspired by Django. While it is still in a beta state (0.16 at the time of writing), it already has the main features you are expecting from an ORM.
Inspired by Django, it has a low learning curve, especially if you have some experience with Django. It also supports Foreign Keys, Many-to-Many and One-to-One relations, and the reverse keys are imported into the other model as well.
As one of the only async-first ORMs, it fits nicely into a modern async stack. It also supports typing natively, including its generic QuerySets. As for now, reverse relationships are not yet natively typed, but adding manual typings is supported without hurting any of the logic.
On the downside, as a relatively new framework, it does not support every use case under the sun. It does not have a migration framework yet (though it is looking to create one). This requires you to do quite a bit of extra manual effort in SQL to keep your tables up to date. Additionally, the async syntax takes some getting used to to make sure you get what you want. On the other hand, it does make it very clear what data is present and when you need to query the database, which is obfuscated in for example Django.
Select Tortoise ORM if you are making an async stack. With it still being in Beta, I can’t fully recommend it yet for heavy production use but it is a great tool for smaller personal projects.
All of these frameworks are solid choices. Older frameworks might not take advantage of the newest Python syntax or goodies, but they are reliable, well tested and known throughout the community. You will be able to find support much easier. The newer frameworks do use the newer features helping you with code quality or speed but have fewer integrations and support will be harder to find. It all comes down to personal preference.
For my latest projects, I’ve chosen to use FastAPI and Tortoise. The decision was mainly influenced by looking for a very modern web stack. FastAPI reduces the need for code with built-in validation, auto-generated documentation and native support for static typing. Tortoise ORM was selected because I wanted a Django-like ORM that I could use within FastAPI. It also helps me dabble in Python’s async module to learn more there.
Let me know what you would have chosen in the comments!