Since you're reading this book, you're probably familiar with Python and may even know about some of the popular tools that are used to build web applications using Python. As for Sanic, you've either heard of it or have used it and want to improve your skills and understanding of it. You may know that Sanic is unlike traditional tools. Built 100% from the ground up, it's been developed with the idea of asynchronous Python in mind. From the very beginning, Sanic set out to be fast. It is one of the critical decisions that drives much of its development.
To truly understand the Sanic project, we'd benefit from a history lesson. Sanic was the first legitimate attempt to bring asynchronous Python to a web framework. It started as a proof-of-concept, as a hobby project. So, let's set the stage.
The most fundamental building block of Sanic is the asyncio module from Python's standard library. The Sanic project was born during the early stages of the module's release and has matured alongside the module.
Python 3.4—released in early 2014—was the first step to introduce the concept of coroutines into the standard library in the newly added asyncio module. Using standard Python generators, a function's execution can be halted while something else happens, and then data can be injected back into that function to allow it to resume execution. If there was an object that "looped" through a list of tasks that needed work, we could duck in and out of the execution of multiple functions at the same time. This could achieve "concurrency" in a single thread and is the basis of the idea of asyncio.
In the early days, this was mainly a toy and there was no widespread adoption of coroutines. Of course, legitimate application needs were being solved, but the concept was still very early in its development and not fully developed. The concept was refined over the next several Python releases to give us the asyncio module we know today.
Here's a quick look at what asynchronous programming looked like in Python 3.4:
import asyncio
@asyncio.coroutine
def get_value():
yield from asyncio.sleep(1)
return 123
@asyncio.coroutine
def slow_operation():
value = yield from get_value()
print(">>", value)
loop = asyncio.get_event_loop()
loop.run_until_complete(slow_operation())
loop.close()
While we will not delve into how this works, it is worth mentioning that asynchronous Python is built on the premise of generators. The idea is that a generator function can yield back to some "loop" to allow it to duck in and out of execution.
Important Note
Sanic can be run with the standard library asyncio loop. However, out of the box, it uses an alternative loop called uvloop that operates significantly faster.
The language and syntax from the new asyncio module were both extremely powerful and a bit clunky. Generators are usually a bit mysterious and difficult for less seasoned Python developers. What exactly is yield from
? This stuff looked alien to many people; Python needed a better syntax.
Before continuing, it is worth providing a quick side note if you are unfamiliar with generators. Python has a special type of function that returns a generator. That generator can be used to partially execute a function and suspend its operation by yielding a value until it is needed again. Generators can also be bi-directional, in that data can be sent back into them during execution. Again, the specifics of this are beyond the scope of this book since it is not entirely pertinent, but it is helpful to know that this is what yield from
helps us achieve. Using the bidirectional functionality of generators, Python was able to build the capability for asynchronous coroutines.
Because this implementation was complex and slightly more difficult for newcomers when Python 3.5 was released, it included a much simpler and cleaner version:
async def get_value():
await asyncio.sleep(1)
return 123
async def slow_operation():
value = await get_value()
print(">>", value)
One of the main benefits of this style of programming is that it helps you block code due to input and output. This is known as I/O-bound. A classical example of an I/O-bound application is a web server. So, it was a natural fit for this new asyncio module to be built with the idea of creating protocols for interacting with networking traffic.
At the time, however, there were no frameworks or web servers that adopted this approach. The Python web ecosystem was built upon a premise that is fundamentally at odds with asynchronous Python.
Important Note
The classical Python pattern for integrating an application with a Python web server is known as the Web Server Gateway Interface (WSGI). This is not within the scope of this book. While it is possible to shoehorn Sanic into WSGI, it is generally frowned upon. The problem with WSGI is that the entire premise of it is blocking. A web server receives a request, processes it, and sends a response all within a single execution. This means that the server can only process one request at a time. Using Sanic with a WSGI server destroys the asynchronous ability to efficiently handle multiple requests concurrently.
Classical Django and Flask could not adopt this pattern. Looking back after 6 years, those projects eventually did find ways to introduce async/await. However, it is not the natural use pattern for these frameworks and came at the expense of an extraordinary effort over many years.
As the asyncio module was being released, there was a lack of web frameworks to fill this new use case. Even the concept that eventually came to be known as Asynchronous Server Gateway Interface (ASGI) did not exist.
Important Note
The ASGI is the corollary to WSGI, except for asynchronous Python. It is possible—although not required—to use it for Sanic, and we will discuss it further in Chapter 8, Running a Sanic Server.
In the summer of 2016, Sanic was built to explore the gap. The idea was simple: could we take an application with a simplistic-looking API from Flask and make it async/await?
Somehow, the idea took off and gained traction. It was not a project that was initially set out with the goal of redoing how Python applications handled web requests. It very much was a case of accidental exposure. The project exploded and created excitement very quickly. There was a lot of appeal in making Flask adopt this new pattern; but, since Flask was incapable of doing so itself, many people thought that Sanic could be the async version of Flask.
Developers were excited for this new opportunity to use the latest in Python to bring a whole new level of performance to their applications. Early benchmarks showed Sanic running circles around Flask and Django.
Like so many projects, however, the initial burst of energy died off. The original project was meant to answer the question, "could a Flask-like framework exist?" The answer was a resounding yes. However, being a one-person project that had no intention of handling the level of support and attention that it received, the project started to gather dust. Pull requests started piling up. Issues went unanswered.
Through 2017 and 2018, the ecosystem for asynchronous Python was still very immature. It lacked production-worthy platforms that would be supported, maintained, and viable for both personal and professional web applications. Furthermore, there was still a bit of an identity question about Sanic. Some people felt that it should be a very niche piece of software, while others felt it could have broad applicability.
The result of the months of frustration and lack of responses from the maintainers of the project resulted in the Sanic Task Force. This precursor to the Sanic Community Organization (SCO) was a loose collective of developers that were interested in finding a future for the project, which was on the verge of failing. There was a desire to stabilize the API and answer all of the outstanding identity questions. For several months in mid-2018, a debate brewed about how to move the project forward, and how to make sure the project would not suffer the same fate again.
One of the most fundamental questions was whether the project should be forked. Since no one on the Sanic Task Force had admin access to the repository or other assets—and the only person who did was non-responsive—the only option was to fork and rebrand the project. However, Sanic had already existed for 2 years at that time and was known within the Python community as a viable (and fast) option for building asynchronous web applications. Dropping the existing project name would have been a huge blow toward ever getting the new project back up off the ground. Nonetheless, this was the only remaining solution. On the eve of forking the project to a new GitHub repository, the original maintainer offered up access to the repository and the SCO was born. The team worked to reorganize the operation of the community with the following goals:
- Regular and predictable releases, as well as deprecations
- Accountability and responsibility for responding to issues and support
- Structure for reviewing code and making decisions
In December 2018, the SCO released its first community version of Sanic: 18.12 LTS.
With this new structure in place, the SCO turned to its next question: what is Sanic? Ultimately, the decision was to break with any attempt at Flask parity. While it may be said that the original project was a "Flask clone," this is no longer true. You will still hear it being called "Flask-like," but that is a fair comparison, only because they look similar on the surface. Their features and behaviors are fundamentally different, and the likeness stops there. I try to steer away from this comparison because it diminishes the effort and improvements that hundreds of contributors have made to let Sanic stand on its own. Now that you know some of the history behind Sanic, we turn to one of the goals of this book: making you a better developer.