Raise error for missing variable used in template

While listening to the latest Django Chat podcast with @Lily-Foote there was talk about ways to raise an error when a variable is missing from the template context.

A few ideas were mentioned that I remember:

  1. global setting that changes the behavior for non-Django templates, i.e. Django admin templates use this functionality and so we can’t break that functionality; django-fastdev provides similar functionality now, I think?
  2. a template tag that could be used to define a list of variables that should raise an exception if used and they are not in the context (I think that was the idea?)
  3. similar to #2, but perhaps a way to mark per-template that all variables are required to be in the context?

One thing I was brainstorming was a way to specify, when using the variable, that it should always be non-null. Maybe a suffix added to the variable to indicate that it should be non-null?

In GraphQL, a schema uses a ! to indicate that the value should be non-null, so that was the first thing that popped into my head, but I’m curious if anyone has any other ideas about this?

I originally posted in Mastodon (adamghill: "While listening to the latest @djangochat@fosstod…" - Indieweb.Social), but Lily encouraged me to post it in the forum for other feedback.

Swift also uses ! for force unwrapping of optionals.

1 Like

I think this is a great idea!

I also wonder about adding ? to mark explicitly using the existing behaviour. This would allow Django’s admin, for example, to migrate to use ? where that behaviour is relied on. This would be more self-documenting, which I think should also make overriding/copying admin templates easier.

1 Like

Good point! I do like the explicitedness of the ?.

Requiring a ? for the current behavior seems like it would require a long deprecation period, since I assume there is a lot of template code floating around that expects the current behavior. I guess there could be a setting to determine how it works, but I worry that it could be a messy transition.

I was thinking of it being optional at first - something that people could enforce via a linter or something.

Yeah. I’d suggest not getting too far ahead. The ! (and ? maybe?) idea is cute, a possibility. The idea of swapping out the default behaviour under the ecosystem (with any deprecation period) is a whole something else. (The ?-like behaviour is a defining feature of the DTL. A tag to enforce ! as default for that template, might be a better follow-up)

2 Likes

The simplest way is to apply a template filter that raises if the context value is none.

As you may know, Django templates return none if a specific context is missing.

If you want to find exactly when a specific value is missing during context tracking, you need to customize the template backend.

Could this be a good candidate for the introduction of feature flags?

  • feature off: legacy behaviour
  • feature on: new behaviour inforced

Ansible has this interesting feature (though I am not sure if Jinja doesn’t provide this out of the box) where you would put something like:

#jinja2: trim_blocks:false

at the beginning of a file to change the behavior for that template invocation. We could probably do something similar ala:

#dtl: raise_on_undefined:true

Turns out there’s an only ticket for this, of course. (The raising bit, not the optional/force unwrapping syntax)

There’s a PR for it, but I’m not sure it’ll work, as it’s only currently a simple toggle at the template engine level.

But take a look at Tim’s old comment:6 there. There some ideas and breadcrumbs for how we might make this more subtle and useful (allowing logging for example, rather than just raising).

I think if we could combine that with (maybe) the per-file thoughts from this thread we might be able to make progress on an old and recurring point.

I think we should be pursuing something like Jinja’s undefined types, which allow flexible customization of what to do on missing variable lookups. I once prototyped a DTL monkeypatch that added a similar feature in a client project, and it worked well, flagging missing variables without breaking the admin (!). I have some notes from which I could try to rewrite the code.

I’m not such a fan of adding ! syntax. It seems like it would just converge on a best practice: “always write !” with no real value from having to remember this.

I’m also not a fan of the existing PR introducing raise_on_missing_variable, because it’s a simple toggle.

And whatever we do, I think we should deprecate string_if_invalid when we add some alternative. It’s quite flawed, as I noted in this investigation.

2 Likes

Oooh! Taking inspiration from Jinja’s undefined types could be a really nice approach too! And avoids the syntax overwhelm at the same time.

This is incorrect. By default you get the empty string, not None.

Just FYI, django-fastdev does not break the admin, and does make undefined lookups crash. With the caveat that it allows for undefined variables in {% if ... %} because that’s the way a lot of checks for undefined is done. (Which btw is pretty bad, since if you set the undefined lookup to be “***” for example to show it in your templates, now those {% if %} blocks will evaluate to true!)

It is correct to display an empty text when a nonexistent context is shown.
What I meant is that in the if statement, a is None holds true.

That’s not how Django works though. The failed lookup of a variable results in the empty string by default. In {{ does_not_exist }} AND in {% if does_not_exist is None %}. It’s the same.

The Django Admin could be fixed so that it doesn’t rely on non-existent variables producing empty strings too. I haven’t looked but I’d be surprised if it’s more than 10 places that can be changed to fix this.

The problem is more what do we fix it to? The approach I took in django-fastdev where lookups inside {% if %} and {% for %} produce empty strings like default isn’t great since it also hides real bugs. I tried introducing {% ifexists %} but after trying that myself a while it became clear that was also an inconvenient way forward as you often want to do {% if does_not_exist or does_exist %} which is awkward to rewrite with {% ifexists %}.

There’s a PR that I hope will be completed soon for django-fastdev to make using the |default filter not crash if there’s a non-existent variable. This will make {% if does_not_exist or does_exist %} easily translatable to ``{% if does_not_exist|default:False or does_exist %}` which I hope would be good, but since I haven’t tried it in practice I can’t vouch for that approach.

{% if aaa is None %}123{% else %}234{% endif %}

rending 123.

Hmm, I stand corrected. That’s even more crazy than I thought it was :frowning:

1 Like