Learning Django URL Patterns by Example

2017-07-12 django

FYI: I built a tiny tool for creating URLconf entries that you can use and contribute to.

One of the more complicated parts of learning Django is learning to write URL patterns. This is primarily because the regular expressions used quickly start to look complicated and are not covered in beginner Django courses or tutorials.

The good news is that they're not all that complicated and there are a number of patterns that are easy to reuse once you start to recognise them.

The URL function

urlpatterns = [
    url(r'^index/$', index_view, name='main-view'),

Django's url function is the core of how this all works. You need to pass it two arguments: the regular expression to match and the view.

There are two additional optional keyword arguments: kwargs for passing options to your view, and name for naming your URL. You should always try to name your views.

Additionally, the url function is overloaded with support for includes. You can use the include function to include another module with a urlpatterns list in it.

urlpatterns = [
    url(r'^posts/$', include('posts.urls')),

When Django attempts to find the view for a given path it will look through the list named urlpatterns in your default URLconf (specified in your settings). It's normally a good idea to leave this next to your settings.py file and include other URLconfs from your apps.

Matching strings and paths

So we've already seen two examples of matching "exact" paths. The "regular expression" in this case is nothing more than a string that represents that URL path.

url(r'^robots.txt$', ...),

Two things that you might notice in the above expression:

By leaving off the $ at the end you can match anything that starts with a specific string. This is particularly useful with includes.

url(r'^admin/', include(...))

Matching the primary key

As soon as you start writing your Django app you're going to quickly run into wanting to create a path for a specific instance of a model. We can achieve this by matching the primary key (or pk) of the object in the path.

By default PKs in Django are positive integers. If that's the case for your project matching the object is a simple as using the 0-9 range.

url(r'^posts/(?P[0-9]+)/$', ...),

The (?P ... ) component creates a group within our regular expression. These groups will be passed to our view as arguments.

The [0-9] creates a group of characters that will be matched, and the + means that we'll match one or more of these characters.

We can go a step further and name those groups. You should consider naming groups within your URL patterns as best practice. Once you name the group it will be passed to your view as a keyword argument.

# URL pattern for matching Django primary keys
url(r'^posts/(?P<pk>[0-9]+)/$', ...)

Matching Django's slug field

Often we have other unique fields on our model, such as Django's slug field. Just like the primary key, it's useful to be able to look up an object using this value.

To match the slug we introduce a new regular expression element: \w which is a shortcut meaning "word character". This is a shortcut for the ranges A-Z, a-z, 0-9 and the _ character. Django also allows the - character in slug fields so we need to add that too.

# URL pattern for matching Django slug fields
url(r'^posts/(?P<slug>[-\w]+)/$', ...)

Matching dates and other fixed-length numbers

Django is used to create CMS's a lot (heck, that's how it all started, right?) so matching years and other dates have always been a common use case.

Up until now, we've used the + character to represent "one or more matches" of the patterns we've created. Regular expressions also have the concept of a fixed length match by using the {4} syntax. {4} would match the preceding pattern four times.

# URL pattern for matching four digit years
url(r'^archive/(?P<year>[0-9]{4})/$', ...)

We can use that pattern to create matches for other date formats such as the ISO date format or the simple year, month, day combination often found on news sites.

# URL pattern for matching ISO date format
url(r'^archive/(?P<year>[0-9]{4})/$', ...)

# URL pattern for matching /year/month/day/
url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', ...)

Getting fancy and matching a hexadecimal colour

We can combine some of the information we've learned to match more complicated patterns. Hexadecimal colours are 6 characters long and contain characters from 0-F. That is 0-9 and A-F.

# URL pattern for matching a hexadecimal colour
url(r'^colour/(?P<colour>[0-9A-F]{6})/$', ...)

If we want to get really smart we can introduce the "optional" operator ? to mark the "u" in colour as optional. We're all about being welcoming to our American friends here!

# URL pattern for matching both colour and color
url(r'^colo(u)?r/(?P<colour>[0-9A-F]{6})/$', ...)

We've used the length selector there to say that we want exactly six matches of the previous pattern. It also supports adding upper and lower limits using the {<lower>,<upper>} format.

# URL pattern for matching 3-6 character hexidecimal paths
url(r'^colour/(?P<colour>[0-9A-F]{3,6})/$', ...)

Those aren't all valid colours though! In CSS valid colours have to have either 3 or 6 hexadecimal characters. To do that we have to create two groups separated by the | character.

# URL pattern for matching 3 and 6 character hexadecimal colours
url(r'^colour/(?P<colour>([0-9A-F]{3}|[0-9A-F]{6}))/$', ...)

This allows us to match a three character hex string (like "EEE") or a six character one (like "AE44AE").

Matching optional groups in our URLs

Finally, we'll look at marking whole groups within our URL as optional. Perhaps we want to support looking up a post by its primary key and optionally including the slug in the URL. Nice URLs are hip and cool after all.

Just like making an individual character optional we can mark whole groups as optional with the ? syntax.

# URL pattern to match post with optional slug

The above will support using both /colour/123 and /colour/123/lush-pink as the URL path.

Keep Learning!

I hope the above introduction has given you a little more insight into the power that is Django's URL parsing. As always: there is loads more information in the Django documentation.

Feel free to post any questions on Twitter :)