Easy HTTP status codes in Python
This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar for Advent 2023, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post in the series for an introduction.
The most useful test
I could be misremembering, but I think Frank Wiles was the first person I ever heard explain that, for a web application, the single most useful test you can add to your test suite, and the very first one you should always add, is just a test case that makes a request to a URL (often, the home page or root URL), and verifies that it receives a response with an HTTP status code of 200 OK
. Think of this as a bonus tip for today, because having this test and running it every time you run your test suite will catch so many problems for you. For something so short and simple it really is amazingly useful.
Anyway, here’s how you might write such a test in Django:
from django.test import TestCase
class EndToEndTests(TestCase):
"""
End-to-end tests of the application.
"""
def test_home_page(self):
"""
Test that the home page returns an HTTP "OK" response.
"""
response = self.client.get("/")
assert response.status_code == 200
This looks reasonable, but the hard-coded integer 200
value does stick out a bit. It’s kind of a magic number (in the bad sense), though maybe not fully one because, at least in the context of a web application, HTTP status codes are pretty familiar.
Status symbols
Or are they? Quick: without looking it up, which of 301 and 302 is the permanent redirect and which is the temporary? What do 307 and 308 mean?
Maybe you knew these off the top of your head. Personally, I have to look up the 301/302 distinction every time (301 is the permanent one; Django helpfully provides distinct HttpResponseRedirect
and HttpResponsePermanentRedirect
classes to represent them), and I didn’t even know about 307/308 until they popped up in a security issue (the short explanation is they’re also redirects, but require the original request method be preserved, while user-agents can switch to a GET
when following a 301/302).
And there are a bunch more status codes that pop up once you start doing “REST” or vaguely REST-ish things and start using the full vocabulary of HTTP. So naturally people have turned to named constants. But everybody has invented their own. To write code that returns or checks for an HTTP 200 OK
, for example, you have the choice of:
- Just writing the
200
literal - Naming it yourself in a module of your application
- In Django REST framework, using the
rest_framework.status
module:rest_framework.status.HTTP_200_OK
- In Starlette or Starlette-based frameworks, using the
starlette.status
module:starlette.status.HTTP_200_OK
(FastAPI also exports a copy of thestarlette.status
module asfastapi.status
) - In Litestar, using the
litestar.status_codes
module:litestar.status_codes.HTTP_200_OK
And on and on — those were just the ones I knew of off the top of my head. Wouldn’t it be nice if somebody solved this once and for all?
Luckily, someone has.
A standard for status
Ever since Python 3.5, the standard-library http
module has included the enum http.HTTPStatus
, which is… a standard enum (remember from yesterday’s post) of HTTP status codes. So the test above could be rewritten as:
from http import HTTPStatus
from django.test import TestCase
class EndToEndTests(TestCase):
"""
End-to-end tests of the application.
"""
def test_home_page(self):
"""
Test that the home page returns an HTTP "OK" response.
"""
response = self.client.get("/")
assert response.status_code == HTTPStatus.OK
Need to remember which one is the permanent redirect? It’s HTTPStatus.MOVED_PERMANENTLY
. Someone tried to hit a URL that requires authentication, but they haven’t logged in? Give ‘em an HTTPStatus.UNAUTHORIZED
. Something missing? While most people probably would recognize the 404
easily enough, HTTPStatus.NOT_FOUND
will also do the job.
The advantage of these is that they’re obvious both in the moment as you’re writing the code, and much later as you’re reading and debugging it, and trying to remember things like “what does 405
mean” (it’s HTTPStatus.METHOD_NOT_ALLOWED
).
Also, the HTTPStatus
enum uses the trick mentioned yesterday of attaching extra attributes. Like the standard HTTP phrases for the status codes, and wordier descriptions of them:
>>> from http import HTTPStatus
>>> HTTPStatus.NOT_FOUND.phrase
'Not Found'
>>> HTTPStatus.CREATED.description
'Document created, URL follows'
If you’re using Python 3.12 or newer, there are also properties attached which let you classify codes. For example, if you don’t care which 2xx code you get back, as long as you get one of them, you could do:
def test_success(self):
"""
Test that the URL returns a success response.
"""
response = self.client.get("/some-url")
assert HTTPStatus(response.status_code).is_success
There are also is_informational
, is_redirection
, is_client_error
, and is_server_error
properties.
Go and show off your status
While I appreciate that so many web frameworks and libraries provide their own named constants for the HTTP status codes, I also recommend most developers avoid using them and stick to using the http.HTTPStatus
enum from the standard library whenever possible. I’ve been gradually updating my personal projects to use HTTPStatus
exclusively, for example, and every new project I start at my day job uses it from the beginning. About the only case I can think of for not using HTTPStatus
at this point is if there’s a status code it doesn’t represent, which usually comes up with protocols built on top of HTTP; they may define their own status codes that aren’t in the standard set, and it would probably be best to model those as an enum.IntEnum
containing the appropriate values (though the standard HTTPStatus
covers a few of those — it has the main WebDAV status codes, for example).