A Beginner’s Introduction to Python Web Frameworks

15 min read

beginners introduction python frameworks

So, you’ve started to learn Python.

It doesn’t seem too bad; you can code after all, so it’s just a matter of learning the differences in syntax. Perhaps you’re just at the stage of practicing tutorials and reading books.

But it is high time to start collecting hard experience in Python. It’s time to create your first Python project.

What should you start with? With an idea, obviously, but I’m sure that won’t be a problem. You already have several great concepts waiting for some of your spare time.

What’s next? The choice of a framework. And that’s where the real conundrum starts, because the ecosystem of Python frameworks is quite extensive and varied. Almost like a jungle, if you will.

In this article I’m going describe the most common Python frameworks so you can choose the one you want to start with. Be warned that this is a purely subjective comparison, because it mainly results from my own more-or-less successful attempts to use those frameworks in commercial projects.

Update: Thanks again for your suggestions! Japronto is the newest addition to the list, once more contributed by Sebastian Buczyński.


The most popular Python framework is definitely Django. Its characteristic feature is that within a single package there is everything you need to build a web application, from low- to high-end.

Django applications are based on a design pattern similar to MVC, the so-called MVT (Model-View-Template) pattern. Models are defined using Django ORM. SQL databases are mainly used as storage. Django has a built-in admin panel, allowing for easy management of the database content.

With minimal configuration this panel is automatically generated based on the defined models. Views can include both functions and classes. The assignment of URLs to views is done in one place (the urls.py file), so that after reviewing that single file you can learn what URLs are supported. Templates are created using a fairly simple Django Templates system.

Django is praised for strong community support, and for detailed documentation describing the functionality of the framework. This documentation coupled with the fact that after the installation you get a complete environment, the entry threshold is rather low. After going through the official tutorial, you’ll be able to do most of the things needed to build an application.

Unfortunately, Django’s monolithism also has its drawbacks. It’s difficult, though not impossible, to replace one of the built-in elements with another implementation. For example, using some other ORM (e.g. SQLAlchemy) requires abandoning or completely rebuilding such items as the admin panel, authorization, session handling or generating forms.

Because Django is complete but inflexible, it is suitable for standard applications (i.e. the vast majority of software projects). However, if you need to implement some unconventional design, it leads to a struggle with the framework rather than pleasant programming.

Sample model in Django:

class Company(models.Model): name = models.CharField(max_length=255) email = models.EmailField(max_length=75, null=True, blank=True) website_url = models.URLField(blank=True, null=True) city = models.CharField(max_length=100, null=True, blank=True) street = models.CharField(max_length=100, null=True, blank=True) size = models.IntegerField(null=True, blank=True) date_founded = models.CharField( help_text='MM/YYYY', null=True, blank=True, max_length=7, ) @property def urls(self): return { 'view': reverse('view-company', args=(self.pk,)), 'edit': reverse('edit-company', args=(self.pk,)), } def __unicode__(self): return self.name


Flask is considered a micro-framework. It comes with basic functionality, but also allows you to easily expand it. Therefore, Flask works more as the glue that allows you to join libraries with each other. For example, “pure Flask” does not provide support for any storage, but there are a number of different implementations that you can install and use interchangeably for that purpose (e.g. Flask-SQLAlchemy, Flask-MongoAlchemy, and Flask-Redis). Similarly, the basic template system is Jinja2, but you can use a replacement (e.g. Mako).

The motto of this framework is “one drop at a time”, and this is reflected in its comprehensive documentation. Knowledge of how to build an application is acquired in portions here – after reading a few paragraphs, you will be able to perform basic tasks. You do not have to know the more advanced stuff – you’ll get to learn it only when you need it. Thanks to this, the Flask student can gather knowledge smoothly and avoid boredom, making Flask suitable for learning.

A large number of Flask extensions, unfortunately, are not as well supported as the framework itself. It happens quite often that the plug-ins are no longer being developed or their documentation is not outdated. In such situations, you need to spend some time googling a replacement that offers similar functionality, but is still actively supported. Building your application with packages from different authors, you might have to put quite a bit of sweat into integrating them with each other. You will rarely find ready-made instructions how to do this in the plug-ins’ documentation, but in such situations the flask community and websites such as Stack Overflow may be helpful.

Sample view in Flask:

@image_view.route( '/api/<string:version>/products/<int:prod_id>/images', methods=['GET'], ) @auth_required() @documented(prod_id="ID of a product") @output(ProductImagesSeq) @errors(MissingProduct) @jsonify def images_get(version, prod_id): """Retrieves a list of product images.""" return [i.serialize() for i in find_product(prod_id).images]


A third noteworthy web framework called Pyramid is rooted in two other products which are no longer developed: Pylons and repoze.bfg. The legacy left by its predecessors caused Pyramid to evolve into a very mature and stable project.

The philosophies of Pyramid and Django differ substantially, even though both were born in the same year (2005). Unlike Django, Pyramid is trivial to customize, allowing you to create features in ways that were not foreseen by the authors of the framework. It does not force the programmer to use framework’s idioms. It’s meant to be a solid scaffolding for complex or highly non-standard projects.

Pyramid strives to be persistence-agnostic. There is no bundled database access module, but a common practice is to combine Pyramid with the powerful, mature SQLAlchemy ORM. Of course, that’s only the most popular way to go. Programmers are free to use whatever tool they find useful, such as using peewee ORM instead, writing raw SQL queries or integrating with a NoSQL database, just to name a few. All options are open, though this approach requires a bit of experience to smoothly add the desired persistence mechanism to the project. The same goes for other components, such as templating.

This summarizes what Pyramid is all about. Modules bundled with it relate to the web layer only. Users are encouraged to freely choose 3rd party packages that will support other aspects of their project.

However, this model causes a noticeable overhead at the beginning of any new project, because you have to spend some time choosing and integrating tools that the team is comfortable with. Still, once you put the effort into making extra decisions in the beginning, you are rewarded with a setup that makes it easy to start a new project and comfortable to develop it further.

Pyramid’s motto is "The Start Small, Finish Big Stay Finished Framework". This makes it an appropriate tool for experienced developers who are not afraid of putting a lot of extra work in the beginning, without shipping any feature within the first few days. Less experienced programmers may feel a bit intimidated.

Sample "Hello world" app in Pyramid:

from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response def hello_world(request): return Response('Hello, world!') if __name__ == '__main__': with Configurator() as config: config.add_route('hello', '/') config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() server = make_server('', 6543, app) server.serve_forever()


Born in 2007, web2py is a framework that was originally designed as a teaching tool for students, so the authors’ main concern was ease of development and deployment.

web2py is strongly inspired by Django and Ruby On Rails, sharing the idea of convention over configuration. In other words web2py provides many sensible defaults that allow developers to get off the ground quickly.

This approach also means there are a lot of goodies bundled with web2py. You will find everything that you expect from a web framework in web2py, including a built-in server, html generating helpers, forms, validators and many more - nothing unusual so far. Support for multiple database engines is nice, though a quite common asset among current web frameworks.

However, some other bundled features may surprise you, since they are not present in other frameworks:

  • helpers for creating javascript-enabled sites with jQuery and Ajax,
  • scheduler and cron,
  • 2-factor authentication helpers,
  • sending SMS messages,
  • an event ticketing system, allowing for automatic assigning of problems that occurred in production environment to a developer!

The framework proudly claims to be a full-stack solution, providing everything you may ever need.

web2py has extensive documentation available online. It guides newcomers step by step, starting with a short introduction to the Python language. The introduction is seamlessly linked with the rest of the manual that demonstrates different aspects of web2py in a friendly manner, with a lot of code snippets and screenshots.

Despite all its competitive advantages, web2py's community is significantly smaller than Django's or even Pyramid's. Fewer developers using it means your chances of getting help and support are lower. The official mailing list is mostly inactive.

Unfortunately, at the moment of writing this section (January 2018), web2py is still not compatible with Python 3. This state of affairs puts the framework's prospects into question, as support for Python 2 ends in 2020. This issue is being addressed at the project's github. You can track progress here.

Sample model in web2py from the Movuca project:

class Ads(BaseModel): tablename = "ads" def set_properties(self): T = self.db.T self.fields = [ # main Field("title", "string"), Field("description", "text"), Field("picture", "upload"), Field("thumbnail", "upload"), Field("link", "string"), Field("place", "string") ] self.computations = { "thumbnail": lambda r: THUMB2(r['picture'], gae=self.db.request.env.web2py_runtime_gae) } self.validators = { "title": IS_NOT_EMPTY(), "description": IS_LENGTH(255, 10), "picture": IS_IMAGE(), "place": IS_IN_SET(["top_slider", "top_banner", "bottom_banner", "left_sidebar", "right_sidebar", "inside_article", "user_profile"], zero=None) }


Sanic differs considerably from the aforementioned frameworks because unlike them, it is based on asyncio - Python’s toolbox for asynchronous programming, bundled with the standard library starting from version 3.4.

In order to develop projects based on Sanic, one has to grasp the ideas behind asyncio first. This involves a lot of theoretical knowledge about coroutines, concurrent programming caveats and carefully reasoning about the data flow in the application.

Once a developer gets the idea and applies Sanic/asyncio to an appropriate problem, the effort pays off. Sanic is especially handy when it comes to coping with long-living connections, such as websockets.

Another use case is writing a “glue-web application”, that can be a mediator between two subsystems with incompatible APIs.

On the other hand, Sanic will not be a good choice for simple CRUD applications that only perform basic database operations. It would just make them more complicated with no visible benefit.

Sanic is meant to be very fast. One of its dependencies is uvloop - an alternative, drop-in replacement for asyncio’s not-so-good built-in event loop. uvloop is a wrapper around libuv. The same engine powers node.js. According to the uvloop documentation, this makes asyncio work 2-4 times faster.

In terms of “what’s in the box”, Sanic does not offer as much as other frameworks. It is a micro-framework, just like Flask. Apart from routing and other basic web-related goodies like utilities for handling cookies and streaming responses, there is not much inside. Sanic imitates Flask, for example by sharing the concept of Blueprints - tiny sub-applications, letting developers split and organize their code in bigger applications.

Sanic is a great choice for projects requiring support for websockets or making a lot of long-lasting external API calls. Please note it requires at least Python 3.5.

Handling websockets in Sanic:

@app.websocket('/websocket') async def time(websocket, path): while True: now = datetime.datetime.utcnow().isoformat() + 'Z' await websocket.send(now) await asyncio.sleep(random.random() * 3)


Have you ever considered it to be possible to handle 1,000,000 requests per second with Python? It is difficult to imagine given Python is not the fastest programming language out there. When a brilliant move was made to add asyncio to the standard library, it opened countless possibilities.

Japronto is a micro-framework that leverages some of them. As a result, this Python library was able to cross the magic barrier of 1 million requests handled per second.

First of all, it uses uvloop - an asyncio backend based on libuv.  The second ace up Japronto's sleeve is picohttpparser - a lightweight HTTP headers parser written in C. All core components of the framework are also implemented in C. A wide variety of low-level optimizations and tricks are used to tweak performance.

Japronto is meant to provide a solid foundation for microservices using REST APIs with minimal overhead. In other words, there is not much in the box. A developer needs only to set up routing and decide which routes should use synchronous or asynchronous handlers. It might not be obvious, but if a request can be handled in a synchronous way, one should not try to do it asynchronously, as the overhead of switching between coroutines will limit performance.

Japronto is designed for special tasks that could not be accomplished with bloated mainstream frameworks. It is a perfect fit for problems where every nanosecond counts. A knowledgeable developer, obsessed with optimization, will reap all of its possible benefits.

It is a bit unfortunate that Japronto is not being actively developed. On the other hand, the project is licensed under MIT, and the author claims he is willing to accept any contributions. Like Sanic, it is meant to work with Python 3.5+ versions.

Hello world in Japronto:

from japronto import Application def hello(request): return request.Response(text='Hello world!') app = Application() app.router.add_route('/', hello) app.run(debug=True)


The world of Python frameworks includes many more interesting examples. Each of these frameworks focuses on a different issue, was built for distinct tasks, or has a particular history.

One that comes to mind is Zope2, one of the oldest frameworks which is still used mainly as part of the Plone CMS. Zope3 (later renamed BlueBream) was created as Zope2’s successor. The framework was supposed to allow for easier creation of large applications, but has not won too much popularity, mainly because of the need to master fairly complex concepts (e.g. Zope Component Architecture) very early in the learning process.

Also noteworthy is Google App Engine, which allows you to run applications written in Python, among others. This platform lets you create applications in any framework compatible with WSGI. The SDK for App Engine includes a simple framework called webapp2 and exactly this approach is often used in web applications adapted to this environment.

Another interesting example is Tornado developed by FriendFeed and made available by Facebook. This framework includes libraries supporting asynchronicity, so you can build applications supporting multiple simultaneous connections (e.g. long polling, WebSocket). Other similar libraries include Pulsar (async), Twisted (callbacks), and Gevent (greenlet). These libraries allow you to build any network applications (e.g. multiplayer games, chat rooms), but but they also perform well at handling HTTP requests.

Developing applications using these frameworks and libraries is more difficult and requires you to explore some more difficult concepts. I’d recommend using them later on in your venture into the Python world.


I hope this short summary of Python frameworks will help you decide which framework you should delve into first.

Now then, it’s time for your move. Read a tutorial, write some practice code and then get started with your first project in Python.

And if you have any more Python-related questions, don't hesitate to leave us a comment or contact us - we'd be happy to answer.

nearshoring ebook

Wojciech Lichota

Head of Service Delivery


Sebastian Buczyński

Senior Python Developer

More articles about Python