A nice way to deal with query strings in Python

TL;DR: I accidentally wrote an argparse for web framework views. You can get it here.

What?

Do you care about your query strings? Do you like them to look nice? Do you find yourself repeatedly writing code to validate parameter values?

Well, I recently got annoyed with repeatedly solving those problems for myself in my Django-based site and wrote a nice solution that I thought others might find useful.

Why?

First, let me lay out the problems I wanted to solve:

  1. I’d like to omit parameters that are already the default.
  2. I’d like to have the parameters in a preferred order.
  3. I kept having to write code to cast GET parameter values into a certain type and check that they’re valid.
  4. My code didn’t make it very clear what the potential parameters were for each view, what their types were, and what their defaults were.

To expand on #1: my webapps often use query strings. But it’s true that query strings are ugly and I’d like beautiful urls. So when possible, I’d like to minimize the query strings by omitting default parameters. Most of the time, my apps need few or no non-default parameters, so this point is significant.

How?

I ended up with a utility object called QueryParams. To use it, first, you define your parameters:

def view(request):
  params = QueryParams()
  params.add('page', type=int, default=1, min=1)
  params.add('format', default='html', choices=('html', 'plain'))
  params.add('admin', type=boolish, default=False, choices=(True, False))
  params.add('showdeleted', type=boolish, default=False, choices=(True, False))

I really like how this creates a straightforward declaration of the parameters your view accepts, right at the top of the function. Each call to add() adds a parameter, and it keeps track of the order you add them. Just like in argparse, the type argument can be any function which will convert the raw string into the correct type. The boolish type is a helper function that converts '', 'False', 'false', and '0' into False, and 'True', 'true', and '1' into True (and throws a ValueError on anything else). The choices argument also functions like in argparse. There’s also min and max arguments, which declare the upper and/or lower limits to a numeric parameter.

Then, you give it the query parameters from the request:

  params.parse(request.GET)

The argument can be any dict-like object. If the value for any parameter throws a ValueError or TypeError when converting, or if it’s not one of the given choices, or if it violates the min or max, then the default value is used instead. In that case, the params.invalid_value attribute will be set to True so you can tell there was invalid input.

The QueryParams class is actually a subclass of collections.OrderedDict, storing the parameters. So after it parses your parameters, the params object can be used to store and manipulate the values of the parameters:

  if params['format'] == 'plain':
    return HttpResponse(text, content_type='text/plain; charset=utf-8')
  if params['admin'] and not is_admin(request):
    params['admin'] = False

Then, at the end, to generate a query string for a url, you can just call str(params).​ This will convert the parameters into a prettified query string, in the order you declared them, and with defaults omitted. And yes, it includes the '?', so you can tack it right onto the end of your url path. If all parameters are the default, so no query string is required, it will just return ''.

But, of course, when you generate links for your page, you’re usually not linking to the same page you’re already on! You’d like to link to the next page of search results, or the same list but with, say, hidden things shown. So I added the convenience function but_with(). Weird name? Let’s use it in a sentence:

  to_next_page = f'{our_url}{params.but_with(page=3)}'

I particularly like how English-sounding it ends up. It returns a copy of the params object, so you don’t have to worry about it altering the original.

Summary

This ended up being a more successful tool than I expected, and it’s made my code vastly simpler and more straightforward. I use it in Django, but this should work any time you’re dealing with query strings and can access them in a dict-like object. If you’d like to use it yourself, just copy the code here. I consider it public domain, but if you’d like to be nice, I’d appreciate credit if you use it.

Leave a comment