Django spotlight: SimpleLazyObject & Co.

We probably all know the ins and outs of Django models, and the settings we need to tweak before heading to production. But Django's packed with tiny bits of useful, lesser known constructs and functionality you can safely reuse. Like SimpleLazyObject.

Its purpose

The docstring of SimpleLazyObject is quite brief:

A lazy object initialized from any function.

Designed for compound objects of unknown type. For builtins or objects of known type, use django.utils.functional.lazy.

So first: what's lazy?

Lazy evaluation isn't something unique to Django. It's a technique that allows postponing (expensive) code evaluation until the result is actually required.

A simple example in Python: generators. Similarly the range type in Python does not allocate a list. Meaning the cost of calling range(1000000) or range(3) is effectively the same. That would not be the case if range was a function that returned a list. The first call would require vastly more memory and processing power.

Another example you've probably been using in Django every single day: QuerySets. And perhaps string translations, through functions like gettext_lazy.

One more? request.user! The User isn't actually retrieved from storage until you access its properties. How? Using SimpleLazyObject.

Here's the code for Django's AuthenticationMiddleware:

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if not hasattr(request, "session"):
            raise ImproperlyConfigured("...")
        request.user = SimpleLazyObject(lambda: get_user(request))
        request.auser = partial(auser, request)

As soon as you call request.user.is_authenticated, or any other property of the user, SimpleLazyObject will invoke the callable passed to its constructor. In this case that in turn will execute the get_user function, which either retrieves and returns the current user or an AnonymousUser. Django effectively always provides you with an easy way of retrieving the current user whenever you want, though you only incur the cost of the SQL query when you do use it.

Why use it?

Use SimpleLazyObject, or one of Django's other lazy helpers, when:

  1. You want to pass values that may not be needed in every scenario.
  2. You want to defer work until all input is available.

Pass (expensive) values

The set up of request.user perfectly demonstrates the first point. As another example, in my projects I often create a Context class which contains at least the current user and the "current" time. The context for the request is set with custom middleware. Or rather: the SimpleLazyObject wrapping the context is set. I don't actually need that context in every single view. I can just grab it whenever I do need it.

Defer work

The second point has plenty of examples in Django. The most notable are reverse_lazy and gettext_lazy and its brethren. You want to defer the actual translation of a string until the very last moment; i.e. when you're about to display it to the user in one way or another.

That's why you can, in general, safely use gettext in a view, while you need to use gettext_lazy when defining the verbose name of a model or model field. If you didn't, the result would be a string in whatever the active language was when the code is loaded.

Here's a fun fact: gettext_lazy = lazy(gettext, str). Yes! That single line is the actual definition of gettext_lazy in Django's codebase.

Likewise, the reverse_lazy function enables you to define a URL reversal without actually reversing the URL at that point in time. Meaning Django won't grind to a halt because you inadvertently called reverse at startup, before the URLConf was properly loaded.

Want to guess the definition of reverse_lazy? Yep. That's right: reverse_lazy = lazy(reverse, str).

Pitfalls

This doesn't mean you should start sprinkling lazy constructs all over your codebase. Just be aware it exists and use the lazy function, LazyObject and their variants where it makes sense.

Unexpected evaluation

The Zen of Python states "explicit is better than implicit". And, boy, were they right.

Spot the difference in these two dummy functions:

def transform_1(queryset, transformer):
    if queryset:
        return [transformer(i) for i in queryset.filter(active=True)]
    return []

def transform_2(queryset, transformer):
    if queryset is not None:
        return [transformer(i) for i in queryset.filter(active=True)]
    return []

The second simply checks if the queryset is None. Easy, fast, explicit.

The first, however, will (1) check if the queryset is None and, if it's not, (2) call its __bool__ method. Which will in turn evaluate the queryset and execute the query before you need to. And since we're then further filtering the queryset, that's an unexpected, additional and useless SQL query.

You really need to be explicit about your intent when dealing with lazy objects.

Type and isinstance

What output would you expect to see in your terminal, assuming the user is anonymous?

def check_ctx(request):
    user = request.user
    print(type(user))
    print(user.__class__.__name__)
    print(isinstance(user, AnonymousUser))
    # ...

Well, here it is:

<class 'django.utils.functional.SimpleLazyObject'>
AnonymousUser
True

Let's call this bewildering when you don't know what request.user actually is behind the scenes, and slightly confusing even if you do. But hey: if it walks and quacks like a duck... SimpleLazyObject really tries its best to act like a duck, including deferring calls to __class__ to the wrapped object.

Go ahead: be lazy

Django's lazy constructs are very useful tools, best used sparingly. They can improve the performance of certain code paths, and the developer experience, by removing some tedious repetition at little to no extra cost.