Spiria logo.

Django under the influence: a Flask developer’s perspective

September 30, 2020.

When a pythonista devoted to Flask finds himself one day forced to use Django, a framework he used to treat with a bit of disdain until then, he realizes that yes, the grass may sometimes be greener on the other side. Riziq tells us his story:

I’ve been a back-end Python developer for over 7 years. Most of my professional experience has been with the Flask framework, and I’ve been quite happy with it. As far as I am concerned, the two competing options for Web frameworks in Python are Django and Flask (I will probably get hatemail from the Pyramid, Bottle, and other communities for this!). In my mind, I always thought that Flask was objectively superior, as it allows you more freedom of choice, is newer, and is less bloated. I was under the misconception that people chose Django simply due to its popularity. Then, after starting a new position at Spiria, I was handed a few projects that had been developed in Django. I found the experience eye-opening, and have developed an unexpected appreciation for the Django framework. Below, I will outline my assessment of the Django framework, from the point of view of a seasoned Flask developer. You may be reading this post wondering, “Which framework should I use?” Of course, the answer to that question is, as always... it depends!

Project Structure

The Django framework imposes a specific project structure. Your models, views, and routing are all placed in predictable places. The Django projects I inherited at Spiria were all initially created and developed by Rails developers and, though the codebase was sprinkled with idiosyncrasies, I still felt right at home as a Python developer.

I suspect that had the developers started the project in Flask, I would have been in for quite a lot of pain. I remember a conversation I had with a Django developer years ago, about their experience working on a project in Flask. They couldn’t understand how anyone could write a large project in Flask. After diving a bit deeper in the issue, it became clear to me that the developer’s Flask application had absolutely all of the code in a single file. Ten thousand lines in all their glory. No wonder they found the framework unmaintainable! The Flask framework imposes no inherent structure, which can be a double-edged sword. At best, you adopt and enforce a strict structure from the very beginning of your project. At worst, no structure is imposed, and you inherit a very large single-file application, giving new meaning to the concept of single-page applications! This freedom means that every single Flask application you come across will likely be structured in a different way.

Django ORM vs SQLAlchemy

Those of you from a Flask background will most likely have used SQLAlchemy for your Object Relational Mapper (ORM) needs. SQLAlchemy is an extremely powerful framework that gives you as much or as little control over your database as you need. One of the beautiful things about SQLAlchemy is that you can get close to the metal in areas where you really need fine-grained control (for example, areas with poor performance) by tapping into the SQLAlchemy Core and writing queries in their SQL expression language. However, this flexibility does come at a cost; as they say, with great power comes great responsibility. A little lower, I’ll get into some really cool Django ORM features that are likely much harder to achieve in SQLAlchemy due to this flexibility.

In contrast, the feel of Django ORM is much more rail-like, probably due to the fact that Django ORM uses the active record implementation. In essence, this means that your Django model objects will very closely resemble the SQL rows they are derived from. This has advantages as well as disadvantages: on the plus side, the simplicity and elegance of the Django migration system (more below), without having to deal with a database session; on the minus side, not being able to automatically declare joins on relationships right on the model itself. In SQLAlchemy, this is easily done through the “Relationship Loading Techniques” of SQLAlchemy (lazy loading, eager loading, etc). To avoid the N+1 problem, you will need to call select_related() or prefetch_related() everywhere you are querying for that model. This can get tedious, especially when you have two models that are almost always used together.

‘‘‘Example of eagerloading taken from the SQLAlchemy docs’’’
class Parent(Base):
    __tablename__ = ‘parent’

    id = Column(Integer, primary_key=True)
    children = relationship("Child", lazy=‘joined’)

As you can see in the example above, you can bake your joins right on your database model classes, as opposed to appending the join to the query every time you want to optimize your query. See below.

parents_and_children = Parent.objects.select_related(‘children’).all()

Django Migrations vs Alembic

Database migration is one area where Django really outperforms Flask – the built-in database migration system in Django is a pure pleasure to work with. I was particularly impressed when a teammate and I were both committing database migrations into the project at the same time: Django identified the issue immediately, and it was a trivial thing to issue a merge through the Python manage.py makemigrations --merge command. Technically, Alembic has the branches feature to deal with this issue, but it has been in “beta” for years, which does not inspire confidence for production use.

Django Admin / Flask-Admin

Django Admin is a major selling point for Django. Right out of the box, you get an instant CRUD Web interface for all of your database models, tucked away behind a user login screen. The Django Admin panel is quite powerful, and a number of third-party libraries add additional functionality and quality of life enhancements.

A third-party library for Flask, the aptly named Flask-Admin, fits the same niche as Django Admin. In keeping with the overall theme, Flask-Admin comes with much less stuff pre-configured for you. You will need to include some boiler-plate for each database model you would like added to the Flask-Admin panel.

One caveat for both Django Admin and Flask-Admin: while they can save you a lot of time up front, as your project matures and your users start requesting (or demanding) additional features in your admin panels, you will find yourself fighting these libraries to get them to do what you want. I have found these admin libraries best suited for developer use only. If you intend to open up administrative features to your user base, you are better off writing one from scratch, using server-side templating or an API with a front-end framework, like React.

Django REST Framework

Speaking of APIs, I found the Django REST framework to be an absolute dream to work with. The class-based generic views made life so much simpler, and my codebase that much smaller. Basically, you have your views, your serializers and your permissions. Subclass the appropriate generic view (for example, a ListCreateAPIView), attach a default query, a serializer (something that defines your endpoint request/response and handles your conversion to and from JSON to Python), and any permissions that are relevant to that view. That’s it.

‘‘‘Example ListCreateAPIView class’’’
class BlogPostListCreate(ListCreateAPIView):
    permission_classes = [IsAuthenticated, BlogPostPermission]
    serializer_class = BlogPostSerializer
    queryset = BlogPost.objects.all()

In contrast, building this out without the Django REST framework would look something more like this:

from django.http import JsonResponse
from django.views import View

class BlogPostListCreateView(View):

    def get(self, request):
        blog_posts = BlogPost.objects.all()
        blog_post_dicts = []
        for blog_post in blog_posts:
          blog_post_dicts.append({
              ‘id’: blog_post.id,
              ‘title’: blog_post.title,
              ‘body’: blog_post.body
          })
        return JsonResponse({‘blog_post_dicts’: blog_post_dicts})

    def post(self, request):
        new_blog_post = BlogPost.objects.create(title=request.POST[‘title’],
                                                body=request.POST[‘body’])
        return JsonResponse({‘message’: ‘Blog Post Created’})

The example above does not include permission checking, validation, or pagination, which you would get for free with the Django REST framework.

An added bonus is all of the additional libraries that were developed around the Django REST framework, that make your life as a developer so much easier. Two notable libraries I’ve used with the Django REST framework are Django-Rest-Framework-Camel-Case and drf-yasg. The former ensures your API endpoints can accept both CamelCase and snake_case keys on the request and uses CamelCase for the response, all the while maintaining snake_case variable names throughout your Python code. This allows your front-end developers to use the variable naming conventions that they are used to, and allows your back-end Python developers to use snake_case and conform to PEP8.The latter library, drf-yasg, works well with the former, and helps to automatically generate swagger documentation by parsing out your generic views and their serializers. Nothing beats automatically-generated documentation!

To be fair to Flask, a number of third-party libraries likely do a lot of what the Django REST Framework does with Django. However, for some reason, I never reached for one of those tools during my time developing in Flask, which warrants further investigation in itself.

Conclusion

I’m grateful for the opportunity of trying out Django in a professional setting. Prior to joining Spiria, I was a die-hard Flask developer and did not look at the other side of the Python Web framework coin. Flask is an amazing framework, and the freedom it provides allows you to do so much. However, this freedom demands a lot of discipline, and a wrong decision at the architecture stage can cost you dearly as your project matures. Moving to Django opened up my eyes to a lot of quality of life enhancements that I didn’t know I was missing out on in Flask. Of course, there were a few things I missed from Flask (automatic joined loads, for example), but, for the projects I’ve worked on so far, I really did feel like Django was the right choice.

My recommendation would be to reach for Flask when making a quick prototype, or when producing something so customized that Django’s rails would be too restrictive. On the other hand, reach for Django for those projects that can benefit from having that much structure, especially when working with a large team. There is definitely a place for both Flask and Django in the world, and the Python community as a whole is better off for having these choices. Thank you to everyone who contributed to both projects and all the open-source satellite projects that help make both frameworks a dream to work with.