Three uses for functools.partial() in Django

I am rather partial to a plant-scroll.

Python’s functools.partial is a great tool that I feel is underused.

(If you don’t know what partial is, check out PyDanny’s explainer.)

Here are a few ways I’ve used partial in Django projects.

1. Making reusable fields without subclassing

It’s common to have field definitions the same across many models, for example a created_at field tracking instance creation time. We can do create such a field like so:

from django.db import models


class Book(models.Model):
    created_at = models.DateTimeField(
        default=timezone.now,
        help_text="When this instance was created.",
    )

Copying this between models becomes tedious, and makes changing the definition hard.

One solution to this repetition is to use a base model class or mixin. This is fine, but it scatters the definition of a model’s fields, prevents local customization of arguments, and can lead to complex inheritance hierarchies.

Another solution is to create a subclass of DateTimeField and add it to every model. This works well but can lead to complications with migrations, as the migration files will refer to the subclass by import and we will need to update them all if we refactor.

We can instead use partial to create a “cheap subclass” of the field class. Our models will still directly use DateTimeField, but with some arguments pre-filled. For example:

from functools import partial

from django.db import models


CreatedAtField = partial(
    models.DateTimeField,
    default=timezone.now,
    help_text="When this instance was created.",
)


class Book(models.Model):
    ...
    created_at = CreatedAtField()


class Author(models.Model):
    ...
    created_at = CreatedAtField()

partial also allows us to replace the pre-filled arguments, so we can override them when needed:

class DeletedBook(models.Model):
    originally_created_at = CreatedAtField(
        help_text="When the Book instance was originally created.",
    )

We can also apply this technique to fields in forms, Django REST Framework serializers, etc.

2. Creating many upload_to callables for FileFields

When using the model FileField, or subclasses like ImageField, one can pass a callable as the upload_to argument to calculate the destination path. This allows us to arrange the media according to whatever scheme we want.

If we are using multiple such fields that share some logic for upload_to, we can use partial to avoid creating many similar functions. For example, if we had a user model that allows uploading two types of image into their respective subfolders:

from functools import partial

from django.db import models


def user_upload_to(instance, filename, category):
    return f"users/{instance.id}/{category}/{filename}"


class User(models.Model):
    ...
    profile_picture = models.ImageField(
        upload_to=partial(user_upload_to, category="profile"),
    )
    background_picture = models.ImageField(
        upload_to=partial(user_upload_to, category="background"),
    )

3. Pre-filling view arguments in URL definitions

Update (2021-05-05): As pointed out by FunkyBob on Twitter, this use case can also be solved with the kwargs argument of path().

Sometimes we might have a single view that changes behaviour based on the URL mapping to it. In such a case we can use partial to set parameters for the view, without creating wrapper view functions. For example, imagine we have a user profile view, for which we are currently working on “version two” functionality:

def profile(request, user_id, v2=False):
    ...
    if v2:
        ...
    return render(request, ...)

We can map two URLs to the profile function view, switching v2 to True for the a preview URL:

from functools import partial

from django.urls import path

from example.core.views import public


urlpatterns = [
    ...,
    path("profile/<int:user_id>/", public.profile),
    path("v2/profile/<int:user_id>/", partial(public.profile, v2=True)),
]

Fin

I hope this has taught you something new—at least partially.

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

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

Related posts:

Tags: ,