Python’s convenience and versatility mean that it’s used to build software in nearly every walk of IT life. One major niche is web services, where Python’s speed of development and flexible metaphors make it easy to get websites up and running quickly.
And just as you might guess, Python gives you plenty of choices and latitude in web frameworks, both small and large. After all, not every web project needs to be enterprise-scale.
Most should be just big enough to get the job done, and no bigger. This article surveys eight of the best-known Python frameworks that emphasise simplicity, lightweight delivery, and a tight focus.
1 – Bottle
Bottle could be considered a kind of mini-Flask, as it’s even more compact and succinct than that other “microframework.” Due to its minimal footprint, Bottle is ideal for including in other projects or for quickly delivering small projects like REST APIs. Flask is discussed below.
The entire codebase for Bottle fits in a single file and has absolutely no external dependencies. Even so, Bottle comes equipped with enough functionality to build common kinds of web apps without relying on outside help.
The routing system in Bottle, which maps URLs to functions, has almost exactly the same syntax as Flask. You’re not limited to a hard-wired set of paths, either; you can create them dynamically. Request and response data, cookies, query variables, form data from a POST action, HTTP headers, and file uploads can all be accessed and manipulated by way of objects in Bottle.
Each capability has been implemented with good attention to detail. With file uploads, for instance, you don’t have to rename the file if its naming convention clashes with the target file system (such as slashes in the name on Windows). Bottle can do that for you.
Bottle includes its own simple HTML templating engine. Again, though minimal, the templating engine has all of the essentials. Variables included in a template are rendered with safe HTML by default; you have to indicate which variables are safe to reproduce literally.
If you’d rather swap out Bottle’s template engine for a different one, such as Jinja2, Bottle lets you do so without fuss. I prefer the simple-template system bundled with Bottle; it’s fast, its syntax is unpretentious, and it allows you to intermix code and template text without undue difficulty.
Bottle even supports multiple server back ends. It comes with its own built-in mini-server for quick testing, but also will support generic WSGI, a wide variety of WSGI-compatible HTTP servers, and plain old CGI if needed.
Bottle doesn’t need as much documentation as other frameworks, but the docs are by no means skimpy. All of the crucial stuff fits on a single (albeit long) web page. Beyond that, you’ll find full documentation for each API, examples for deploying on various infrastructures, an explanation of the built-in templating language, and a slew of common recipes.
As with Flask, you can expand on Bottle’s functionality manually or via plug-ins. Bottle plug-ins are nowhere near as numerous as Flask’s, but there are useful pieces, such as integration with various database layers and basic user authentication. For async support, Bottle can use one of the existing server adapters that runs asynchronously, such as aiohttp/uvloop, but
async/await is not natively supported.
One consequence of Bottle’s minimalism is that some items simply aren’t there. Form validation, including features like CSRF (cross-site request forgery) protection, isn’t included. If you want to build a web application that supports a high degree of user interaction, you’ll need to add that support yourself.
Another issue with Bottle is that development has stalled; the last point release, 0.12, arrived in 2013. That said, Bottle continues to be maintained, and its development releases remain usable for production. The developers intend to deliver new versions that shed support for legacy editions of Python.
2 – CherryPy
CherryPy has been around in one form or another for almost 20 years, but hasn’t lost the minimalism and elegance that distinguished it from the start.
The goal behind CherryPy, aside from containing only the bare bits needed to serve web pages, is to feel, as far as possible, not like a “web framework” but like any other kind of Python application. Sites like Hulu and Netflix have used CherryPy in production because the framework provides a highly unobtrusive base to build on. CherryPy uses pooled threads under the hood, the better to support multithreaded server adapters.
CherryPy lets you keep your web application apart from the core logic. To map your application’s functions to URLs or routes served by CherryPy, you create a class where the namespaces of the objects map directly to the URLs you want to serve. For instance, the root of the website is provided by a function named “index.” Parameters passed to those functions are used to handle variables provided by GET or POST methods.
The bits that CherryPy includes are meant to work as low-level building blocks. Session identifiers and cookie handling are included, but HTML templating is not. Like Bottle, CherryPy offers a way to map routes to directories on-disk for static file serving.
CherryPy will often defer to an existing third-party library to support a feature rather than provide it natively. WebSocket applications, for instance, aren’t supported by CherryPy directly, but through the ws4py library.
The documentation for CherryPy includes a handy tutorial walk-through of the various aspects of the program. It won’t take you through a complete end-to-end application, unlike some other framework tutorials, but it’s still useful. The docs come with handy notes on deployment in virtual hosts, reverse proxying via Apache and Nginx, and many other scenarios.
3 – Falcon
If you’re building REST-based APIs and nothing else, Falcon was made just for you. Lean and fast, with almost no dependencies beyond the standard library, Falcon provides everything you need for REST APIs and nothing more. Falcon 2.0, released in 2019, does away with Python 2.x support and requires at least Python 3.5.
A big part of why Falcon earns the “light and slender” label has little to do with the number of lines of code in the framework. It’s because Falcon imposes almost no structure of its own on applications. All a Falcon application has to do is indicate which functions map to which API endpoints.
Returning JSON from an endpoint involves little more than setting up a route and returning the data via the
json.dumps function from Python’s standard library. Support for async hasn’t yet landed in Falcon, but work is under way to make that happen in Falcon 3.0.
Falcon also employs sane out-of-the-box defaults, so little tinkering is needed for setup. For example, 404s are raised by default for any route that’s not explicitly declared. If you want to return errors to the client, you can raise one of a number of stock exceptions bundled with the framework (such as
HTTPBadRequest) or use a generic
falcon.HTTPError exception. If you need preprocessing or post-processing for a route, Falcon provides hooks for those as well.
Falcon’s focus on APIs means there’s little here for building web apps with conventional HTML user interfaces. Don’t expect much in the way of form processing functions and CSRF protection tools, for instance. That said, Falcon provides elegant options to extend its functionality, so more sophisticated items can be built.
Aside from the above-mentioned hooking mechanism, you’ll find an interface for creating middleware that can be used to wrap all of Falcon’s APIs.
The documentation for Falcon is slender compared to other frameworks, but only because there’s less to cover. The user guide includes a formal step-by-step walk-through of all major features, along with a quick-start section that lets you view sample code with or without annotation.
4 – FastAPI
FastAPI’s name is a good summation of what it does. It’s built to create API endpoints quickly, and it runs fast too.
FastAPI makes use of the Starlette project for its high-speed networking core, but you don’t need to know about Starlette’s internals to use FastAPI. You define endpoints in much the same manner as a Flask or Bottle app—use decorators to indicate which functions handle which routes—and then return dictionaries that are translated automatically into JSON.
You can easily override how things are returned. For instance, if you want to return HTML/XML from some endpoints, you can do that by simply returning a custom
Response object. If you want to add custom middleware, you can pop in anything that follows the ASGI standard.
FastAPI makes use of Python’s type hinting to provide constraints on the kinds of data that routes accept. For instance, if you have a route with the type
Optional[int], FastAPI will reject any submissions except integers. You don’t have to add data validation code to your endpoints; you can just use type hints and let FastAPI do the work.
Naturally, some things are left out. There’s no native HTML template engine, for instance, but there’s no shortage of third-party solutions to fill that gap. Same with database connectivity, but the documentation contains details about how to coax certain ORMs (e.g. Peewee) to work with FastAPI’s async behaviours.
5 – Flask
Many discussions about Python web frameworks begin with Flask, and for good reason. Flask is a well established, well-understood framework that is easy to use and quite stable. It’s next to impossible to go wrong using Flask for a lightweight web project or a basic REST API, but you’ll face heavy lifting if you try to build anything larger.
Flask’s central appeal is its low barrier to entry. A basic “hello world” app can be set up in fewer than 10 lines of Python. Flask includes a widely used HTML templating system, Jinja2, to make rendering text easy, but Jinja2 can be swapped out for any number of other template engines (such as Mustache) or you can roll your own.
In the name of simplicity, Flask omits niceties such as a data layer or ORM, and offers no provisions for the likes of form validation.
However, Flask can be expanded through extensions, of which there are dozens, covering many common use cases such as caching, form handling and validation, and database connectivity. This lean-by-default design allows you to start engineering a Flask application with the absolute minimum of functionality, then layer in only the pieces you need when you need them.
Flask’s documentation is genial and easy to read. The quick-start document does an excellent job of getting you started while also explaining the significance of the default choices for a simple Flask application, and the API docs are replete with good examples. Also excellent is the collection of Flash snippets, which are quick-and-dirty examples of how to accomplish specific tasks, such as how to return an object if it exists or a 404 error if it doesn’t.
Flask hit its milestone 1.0 release in 2018, with Python 2.6 and Python 3.3 being the minimum supported versions, and with many of its behaviours finally set in stone. Flask doesn’t explicitly support Python’s async syntax, but an API-compatible variation of Flask called Quart has been spun off to satisfy that demand.
6 – Pyramid
Small and light, Pyramid is well-suited to tasks such as exposing existing Python code as a REST API, or providing the core for a web project where the developer does most of the heavy lifting.
“Pyramid will allow you to become productive quickly, and will grow with you,” says the documentation. “It won’t hold you back when your application is small, and it won’t get in your way when your application becomes large.”
A good way to describe Pyramid’s minimalism would be “free of policy,” a term used in the section of the documentation that discusses how Pyramid shapes up against other web frameworks. Basically, “free of policy” means that which database or which templating language you choose to use is not Pyramid’s concern.
Very little work is needed to build a basic Pyramid application. As with Bottle and Flask, a Pyramid application can consist of a single Python file, apart from the files for the framework itself. A simple one-route API requires no more than a dozen or so lines of code. Most of that is boilerplate like
from … import statements and setting up the WSGI server.
By default, Pyramid includes several items that are common in web apps, but they’re provided as components to be stitched together, not as full-blown solutions. Support for user sessions, for instance, even comes with CSRF protection.
But support for user accounts, such as logins or account management, isn’t part of the deal. You’ll have to roll it yourself or add it through a plug-in. The same goes for form handling and database connections.
Pyramid even provides a way to create templates from previous Pyramid projects to reuse prior work. These templates, called “scaffolds,” generate a Pyramid app with simple routing and some starter HTML/CSS templates. Bundled scaffolds include a sample starter project and a project that connects to databases via the popular Python library SQLAlchemy.
Pyramid’s slender testing and debugging tools do the trick. Bundle the
debugtoolbar extension in a Pyramid app and you’ll get a clickable icon on every web page that generates details about the app’s execution, including a detailed traceback in the event of errors. Logging and unit testing are also present.
Pyramid’s documentation is excellent. In addition to a quick tour of the basics and a tutorial-style walk-through, you’ll find a set of community-contributed tutorials and a cookbook of common recipes. The latter includes deployment techniques for a slew of target environments, from Google App Engine to Nginx.
Pyramid supports both Python 2 and Python 3, but does not use Python 3’s async syntax. If you want to leverage async in Pyramid, see the aiopyramid project, which includes a scaffold for an async-powered “hello world” application.
7 – Sanic
Designed for speed and simplicity, Sanic works with Python 3.6 or higher and uses Python’s
async/await syntax (available as of Python 3.5) to let you create efficient web applications.
As with Flask or Bottle, a basic Sanic “hello world” runs about 10 lines of code, most of it imports and other boilerplate. The key difference is that application routes must be declared as
async def functions, and you must use
await to invoke these functions within your async code. If you’ve written async-powered applications before, you already have the hardest part under your belt.
Many of the mechanisms Sanic uses for handling connections and responses will be familiar. Requests and responses are just objects with familiar-looking properties, like uploaded files, forms, JSON objects, headers, and so on.
Applications with many routes become unwieldy to manage. Sanic addresses this with “blueprints,” objects that can describe groups of routes and allow you to manage them via high-level interface.
Instead of writing each route explicitly, or using an excess of routes with variables, you can write a few blueprints to generically describe how the routes work in your app (e.g.,
/object/object_id/action). Blueprints can have common middleware, which is useful if you want to apply management functionality to some routes but not others.
Sanic also works with protocols other than HTTP. WebSocket endpoints require only a different decorator and a little more internal logic (e.g., awaiting and handling responses). Custom network protocols can be supported by subclassing
Sanic deliberately leaves out functionality like database connectivity and HTML templating, while retaining the features one would use to plug in those capabilities: middleware, centralised application configuration, and so on.
8 – Tornado
Tornado is another tiny framework aimed at a specific use case: asynchronous networking applications. Tornado is well-suited for creating services that open a great many network connections and keep them alive—that is, anything involving WebSockets or long polling. Tornado 6.0 requires Python 3.5 or higher, and drops Python 2 support entirely.
Like Bottle or Falcon, Tornado omits features extraneous to its central purpose. Tornado has a built-in templating system for generating HTML and other output, and provides mechanisms for internationalisation, form handling, cookie setting, user authentication, and CSRF protection. But it leaves out features, like form validation and an ORM, that are mainly for user-facing web apps.
Tornado excels at providing infrastructure to apps that need close control over asynchronous networking. For instance, Tornado provides not only a built-in asynchronous HTTP server, but also an asynchronous HTTP client. Thus, Tornado is well-suited to building apps, such as a web scraper or a bot, that query other sites in parallel and act on the returned data.
If you want to create an app that uses protocols other than HTTP, Tornado has you covered. Tornado provides access to low-level TCP connections and sockets to utilities like DNS resolvers, as well as to third-party authentication services, and it supports interoperation with other frameworks through the WSGI standard. The documentation, which is small but not sparse, includes ample examples for accomplishing all of this.
Tornado both leverages and complements Python’s native functionality for asynchronous behaviours. If you’re using Python 3.5, Tornado supports the built-in
await keywords, which promise to give applications a speed boost. You can also use futures or callbacks to handle responses to events.
Tornado provides a library of synchronisation primitives—semaphores, locks, and so on—to coordinate events between asynchronous coroutines. Note that Tornado normally runs single-threaded, so these primitives aren’t the same as their threading namesakes. However, if you want to run Tornado in parallel processes to leverage multiple sockets and cores, tools are available for doing so.
Tornado’s documentation covers each major concept in the framework and all of the major APIs in the model. Although it includes a sample application (a web crawler), it’s mainly for demonstrating Tornado’s queuing module.