Three more uses for functools.partial() in Django

2021-10-12 I remain partial to a plant-scroll.

I remain convinced that Python’s functools.partial() is underappreciated. Following my previous post, here are three more ways to use partial() with Django.

(For an explanation of partial() see PyDanny’s post.)

1. Customizing render()

Django’s render() shortcut simplifies generating an HTTP Response from rendering a template. If several views need to pass the same arguments, we can wrap the shortcut with partial() to avoid repetition.

For example, imagine we have several views that return text, rather than HTML. We could create our own render_text shortcut that calls render with the content_type argument pre-filled:

from functools import partial
from django.shortcuts import render

render_text = partial(render, content_type="text/plain")


def robots_txt(request):
    ...
    return render_text(request, "robots.txt")


def security_txt(request):
    ...
    return render_text(request, "security.txt")

(See my posts on robots.txt and security.txt.)

We can use this approach with other shortcut functions, or even HttpResponse & its subclasses.

2. transaction.on_commit() callbacks

Django’s examples for transaction.on_commit() use functions, or lambdas:

from django.db import transaction

# With a function


def send_email():
    send_admin_email(
        subject="Author created",
        # ...
    )


transaction.on_commit(send_email)

# With a lambda

transaction.on_commit(
    lambda: send_admin_email(
        subject="Author created",
        # ...
    )
)

In the wild, most on_commit() callbacks only call another function with pre-filled arguments. Using lambda can make this easy, but lambda functions have their drawbacks. We can instead use partial() to bind the arguments to the function:

transaction.on_commit(
    partial(
        send_admin_email,
        subject="Author created",
        # ...
    )
)

3. Database functions

Django’s database functions represent functions that will run in the database. As such, in Python they are classes, rather than functions, but this doesn’t stop us from using partial() with them.

For example, to create a specialized SubStr that extracts the first character from a string, we can do:

from functools import partial

from django.db.models.functions import Substr

FirstChar = partial(Substr, pos=1, length=1)

In use, this looks just like a normal database function:

In [1]: from example.core.models import Author, FirstChar

In [2]: Author.objects.annotate(first_char=FirstChar("name")).filter(first_char="C")
Out[2]: <QuerySet [<Author: Author 'Charles Dickens' (1)>]>

For wrappers using more than one database function, we can’t use partial(). Instead we can combine them with a function. For example, to take the first character and uppercase it:

from functools import partial

from django.db.models.functions import Substr, Upper


def FirstCharUpper(expression):
    return Upper(Substr(expression, pos=1, length=1))

Even though this is a function, I like to stick to the CamelCaes naming convention so it matches the database functions.

Fin

Thank you for reading my less-than-impartial post,

—Adam


If your Django project’s long test runs bore you, my book can help.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django, python