skip to content

In Django templates, when does {{ invoice.discount|default:"none" }} give a wrong answer, and why is default_if_none the fix?

level: juniorimportance: should knowfreq 45%

answer

  1. truthiness, not presence
  2. zero is falsy
  3. Decimal zero included
  4. only None triggers the other

basics

~10 s

The default filter replaces any falsy value, so a legitimate 0, Decimal("0.00"), empty string or empty list shows the fallback. default_if_none replaces only None, so a real zero discount still renders as 0.00.

solid answer

~40 s

Django's `default` filter is literally `value or arg`: it swaps in the argument for **any falsy value**. On an invoice, a discount of `Decimal("0.00")` is falsy, so `{{ invoice.discount|default:"none" }}` prints `none` when the customer was explicitly given no discount, which is a different fact from "discount not applicable". `default_if_none` replaces **only** `None`, so a nullable `DecimalField` renders `none` when it is NULL and `0.00` when it is zero. Use `default` for text where empty and missing mean the same thing, such as a blank PO number, and `default_if_none` for numbers, booleans and anything where `0`, `False` or `""` is a real value. Both also receive the empty string that a missing variable resolves to, where only `default` substitutes.

code

python · 9 lines
python
from decimal import Decimal
from django.template import Context, Template

t = Template('[{{ d|default:"none" }}] [{{ d|default_if_none:"none" }}]')
for d in (None, Decimal("0.00"), Decimal("5.00")):
    print(t.render(Context({"d": d})))
# [none] [none]
# [none] [0.00]
# [5.00] [5.00]

go deeper

for a junior

Know that default fires on any falsy value and default_if_none only on None, and pick the right one for text versus numbers.

for a middle

Explain the implementation, value or arg versus value is None, and walk through what each does for 0, Decimal zero, False, empty strings and a missing variable.

for a senior

Spot the bug in review: a default on a money or count field that hides real zeros, and fix it with default_if_none or yesno for nullable booleans.

for a principal

Treat NULL-versus-zero as a data-contract question: make the model's meaning explicit and ensure templates, exports and reports render it the same way.

## The two filters Django ships two fallback filters in `django/template/defaultfilters.py`: - **`default`**: "If value evaluates to False, uses the given default." Its implementation is one line, `return value or arg`. - **`default_if_none`**: "If (and only if) value is None, uses the given default." Its implementation tests `value is None`. The difference is **truthiness versus identity with `None`**. Python treats many legitimate values as falsy, so `default` replaces far more than "missing" data. ## Which values trigger which filter | Value | Falsy? | `\|default:"none"` | `\|default_if_none:"none"` | |---|---|---|---| | `None` | yes | `none` | `none` | | `0` or `Decimal("0.00")` | yes | `none` | `0` / `0.00` | | `False` | yes | `none` | `False` | | `""` (empty string) | yes | `none` | empty | | `[]` or an empty QuerySet | yes | `none` | the empty value | | `Decimal("5.00")` | no | `5.00` | `5.00` | ## The invoicing bug An invoice model has a nullable discount: ```python from django.db import models class Invoice(models.Model): number = models.CharField(max_length=20, unique=True) discount = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True ) po_number = models.CharField(max_length=40, blank=True) ``` `NULL` means "no discount scheme applies to this customer"; `0.00` means "the scheme applies and this invoice earned nothing". The template author writes `{{ invoice.discount|default:"none" }}`, and every zero-discount invoice now claims there is no scheme at all. Accounting reports built from the printed page disagree with the database, and nobody sees an error because the filter did exactly what it is documented to do. The fix is to state the intent: ```django <td>{{ invoice.discount|default_if_none:"n/a" }}</td> <td>{{ invoice.po_number|default:"not supplied" }}</td> ``` The discount distinguishes `NULL` from zero; the PO number, a `CharField` stored as `""` when blank, is a case where empty and missing really do mean the same thing, so `default` is right there. ## Missing variables and the two filters If the variable name itself does not exist in the context, the engine resolves it to its `string_if_invalid` value, an empty string by default, before any filter runs. The empty string is falsy but is not `None`, so: 1. `{{ typo|default:"none" }}` prints `none`, hiding the typo behind a plausible fallback. 2. `{{ typo|default_if_none:"none" }}` prints nothing, because `""` is not `None`. That asymmetry sometimes surprises people who expect `default_if_none` to cover missing names too. ## Choosing between them - **Text fields where blank means absent**: `default` is concise and correct. - **Numbers, money, counts and percentages**: `default_if_none`, because zero is data. - **Booleans**: neither filter is ideal for display; `yesno` maps `True`, `False` and `None` to three different strings, for example `{{ invoice.paid|yesno:"paid,unpaid,unknown" }}`. - **Collections**: to render a message for an empty list, the `{% empty %}` clause of `{% for %}` is usually clearer than a filter. ## Chain order changes the answer Where `default` sits in a chain matters as much as which filter you pick. Formatting first turns a zero into a non-empty string: - `{{ invoice.discount|floatformat:2|default:"none" }}`: `floatformat` turns `Decimal("0.00")` into the string `"0.00"`, which is truthy, so `default` leaves it alone; for `None`, `floatformat` returns `""` and `default` substitutes `none`. - `{{ invoice.discount|default:"none"|floatformat:2 }}`: `default` sees the falsy zero first and substitutes `none`, which `floatformat` then cannot parse and turns into an empty string. The first chain happens to behave like `default_if_none` for these two inputs; the second loses the zero entirely. Relying on that accident is fragile, so state the intent with `default_if_none` and format afterwards. ## Testing the choice A short render test pins the behaviour: render the invoice row once with `discount=None`, once with `Decimal("0.00")` and once with a positive amount, and assert the three outputs differ. The test documents the NULL-versus-zero contract for the next person who edits the template. ## What interviewers are checking This question is less about memorising two filter names than about recognising that `value or default` is a **truthiness test**. A candidate who immediately asks "can the value legitimately be zero or empty?" shows the habit that prevents a whole family of display bugs, in templates and in Python code alike. A strong answer also mentions that the choice belongs in review: a `default` on a numeric field is worth a comment every time.

  • What does {{ typo|default_if_none:"none" }} render when typo is not in the context?
    Nothing, with default settings. A missing variable resolves to the engine's `string_if_invalid`, an empty string by default, before filters run. The empty string is not `None`, so `default_if_none` passes it through unchanged. `default` would print `none`, because the empty string is falsy.
  • How would you display a nullable boolean such as invoice.paid with three distinct labels?
    Use the `yesno` filter with three comma-separated labels: `{{ invoice.paid|yesno:"paid,unpaid,unknown" }}` renders the first for `True`, the second for `False` and the third for `None`. With only two labels, `None` is treated like `False`. Neither `default` nor `default_if_none` can produce three distinct outputs.

saying these in an interview costs you the question

  • default only triggers when the value is None
  • default_if_none also replaces empty strings
  • A Decimal zero is truthy, so default leaves it alone
  • default and default_if_none are interchangeable for numbers
  • default_if_none catches a misspelled variable name