Django is a great framework for web development. Unfortunately, due to the nature of Python and web development in general, it often leads to hard to track down bugs and tangled code. We will take a look at one way to reign this in.
Throughout this series we will follow the official Django tutorial while making the code safer and easier to reason about. We will accomplish this by enlist the help of mypy (for general type checking) and returns (for containers that will provide safety).
I will not cover the basics of Django or Python. If you ever feel like you don't fully understand the Django portion, you can refer to the official tutorial. The parts of this tutorial will directly correspond to the parts of the official tutorial.
I will explain how we're integrating
returns and the benefits of jumping through some of the extra hoops that it requires. I will not cover the underlying theory behind returns. For that, you can read their excellent documentation.
What makes code safe?
There are two properties that make a piece of code safe. It has to be pure and complete.
As an example,
2 + 2 is a pure and complete piece of code. Every time we run this code, we will always get back
4. If we wanted to, we could even replace this code with
4 itself. This is easy to reason about, easy to understand, and easy to test. These properties make a piece of code pure and complete.
Things get complicated, however, once we introduce functions calls. If we have
get_two() + get_two(), some questions immediately start to arise: What kind of thing does
get_two() return? Does
get_two() always return the same thing? Can
We can, of course, read the source code to answer these questions. This gives us full understanding of what the abstraction does, but it quickly becomes time consuming and after going a few layers deep it becomes hard to keep everything in our heads. The other option is to blindly trust that the name of the function fully states what it does. This options lets us proceed with using the abstraction, but it could easily get us into trouble – see the questions above. So what's the solution?
Let us address these problems one at a time.
What kind of thing does
As the code stands, we have not way know that
get_two() returns a
100, 200, or any other crazy thing.
mypy can help here:
This one additions lets us know, right away, that we will get an
int. No accidental strings or tuple can sneak into our code.
get_two() always return the same thing?
This question becomes harder to answer and reason about. This function could return 2 on the first call, but on the second call returns something totally different. This will often happen if a function gets the value from a database, calculates the value based on time, or grabs a random number. If it does any of these things, we have no way of knowing without reading the body.
Let's assume that in this case, we are doing one of the impure things mentioned above. So what can we do about it? Well, we can mark the value as impure by using a special container called
This container tell all the caller that the function does something more than simply calculate a value. It also requires that any function that uses this return value also returns an
IO container. This is a good thing. It lets us know, without reading the source, that we can't depend on the result always being the same. Although, it should still make us want to read the source to figure out what impure thing a function called
get_two() could be doing.
The final question is especially troubling in Python. Python a lot of python code can throw exceptions without warning. Even simple things like dividing by zero could throw an exception that crashes our applications – if not handled properly.
We could wrap everything in a giant
try statement, but wouldn't it be nice to know ahead of time that this function could result in an exception?
Throwing an error makes a function incomplete because it cannot always return the promised value. Just like with
IO we can use the type system to let all the callers know this. The new container we will use is
This container lets the callers know that we could expect an
int or and
Exception. Better yet, it forces the caller to also return a
Result container or to handle the exception.
Starting the project
Use your favorite method to install Django, and let's dive in right where part the official tutorial starts.
We will start the project just like a typical Django project:
Next we need to install some dependencies that will allow us to use the type system to its fullest:
mypyprovides our type system and integrates well with the rest of our dependencies.
django-stubsprovides type stubs for Django.
returnsprovides type safe functional programming facilities.
Once we have the dependencies installed, we need to configure
returns and Django to work smoothly with
We will follow the django tutorial and create the Polls app:
The index view function will look almost the same, except we will add some types.
request a type might seem excessive right now, but when we pass it to a function later on, this will make sure that we pass the right thing.
Before we can access our new view, we need to update a couple
After these steps, we can access the site at http://localhost:8000/polls/ after running:
We covered the goal of this tutorial series along with some basic terminology that we will use throughout. We also setup the base application and got all our dependencies setup. In Part 2, we will actually use the types introduced in
returns and get a glimpse at how they can lead to cleaner code.
If this is your first time using
mypy, you should start seeing some nice type hints coming through. This is already a benefit without needing to do anything different in the code. And if you haven't done so already, make sure to get your editor fully setup for types to work properly.