skip to content

How do you write, register and load a custom Django template filter, such as a money filter for invoice amounts?

level: middleimportance: must knowfreq 50%

answer

  1. a package inside the app
  2. a module-level register object
  3. load by module name
  4. restart after adding it

basics

~20 s

Put a module in the app's templatetags package, create register = template.Library(), decorate a function with @register.filter, and use {% load module_name %} in the template. The app must be in INSTALLED_APPS, and the dev server needs a restart.

solid answer

~40 s

Create `billing/templatetags/__init__.py` and `billing/templatetags/billing_extras.py`. In the module, define `register = template.Library()` and decorate a function with `@register.filter`, or `@register.filter(name="money")` to choose the template name. A filter receives the value and at most one argument, for example `def money(value, currency="EUR")`, used as `{{ line.total|money:invoice.currency }}`. The template must call `{% load billing_extras %}`, which loads by **module** name, not app name, and only works when the app is in `INSTALLED_APPS`; the development server must be restarted after the package is first added. Because the template language has no exception handling, a filter should return a sensible fallback for bad input rather than raise, since a raised exception becomes a server error.

code

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

t = Template(
    "{% load billing_extras %}"
    "[{{ total|money }}] [{{ total|money:cur }}] [{{ missing|money }}]"
)
print(t.render(Context({"total": Decimal("1234.5"), "cur": "USD"})))
# [EUR 1,234.50] [USD 1,234.50] []

go deeper

for a junior

Know the steps: templatetags package with init.py, register = template.Library(), @register.filter, and {% load module_name %} in the template.

for a middle

Explain the discovery rules, INSTALLED_APPS and module-name loading, the one-argument limit, stringfilter, and why a filter returns a fallback instead of raising.

for a senior

Show you design filters as a shared formatting layer: named, tested functions, no database queries in loops, and a clear rule for when a custom tag is the better tool.

for a principal

Decide where formatting policy lives: one shared library for money and dates, versus per-app filters, and how that library is versioned, tested and kept free of business logic.

## Where a custom filter lives Custom filters, and custom tags, live in a **template tag library**: a Python module inside a package named `templatetags` in one of your apps. The package sits next to `models.py` and `views.py` and must contain an `__init__.py` so Python treats it as a package: ```text billing/ __init__.py models.py views.py templatetags/ __init__.py billing_extras.py ``` Three rules follow from how Django discovers libraries: 1. **The app must be in `INSTALLED_APPS`.** Django only scans installed apps for `templatetags` packages; the docs present this as a deliberate restriction on which code templates can reach. 2. **The module name is the library name.** Templates write `{% load billing_extras %}`, not `{% load billing %}`. Pick a name unlikely to clash; two apps defining the same library name trigger system check `templates.W003`. 3. **Restart the development server** after adding the `templatetags` package for the first time; the autoreloader does not pick up a new library on its own. ## Writing the filter A filter is a plain function that receives the **value** and, optionally, **one argument**. The module must expose a module-level variable named `register` that is a `django.template.Library` instance: ```python from decimal import Decimal, InvalidOperation from django import template register = template.Library() @register.filter def money(value, currency="EUR"): try: amount = Decimal(value).quantize(Decimal("0.01")) except (InvalidOperation, TypeError, ValueError): return "" return f"{currency} {amount:,}" ``` In a template: ```django {% load billing_extras %} <td>{{ line.total|money:invoice.currency }}</td> <td>{{ invoice.grand_total|money }}</td> ``` `Decimal("1234.5")` becomes `EUR 1,234.50`. The default for `currency` makes the argument optional, and Django checks the call against this signature when the template compiles. ## Registration options | Form | Template name | Notes | |---|---|---| | `@register.filter` | the function name | the common form | | `@register.filter(name="money")` | `money` | lets the Python name differ | | `register.filter("money", money)` | `money` | the non-decorator call | | `@register.filter(is_safe=True)` and friends | as above | flags that control escaping and time zones | The three keyword flags, `is_safe`, `needs_autoescape` and `expects_localtime`, matter when a filter handles HTML or datetimes; a money filter that returns plain text needs none of them, and its output is autoescaped like any other string. ## Design rules for filters - **Do not raise for bad input.** The template language has no exception handling, so an exception in a filter becomes a server error. Return a fallback such as `""` when the input cannot be formatted, as `money` does for `None`. Raise only when the input represents a clear bug in the template. - **Use `@stringfilter` when you want a string.** The `django.template.defaultfilters.stringfilter` decorator converts the first argument to `str` before your code runs, so `value.upper()` cannot fail on an integer. - **One argument is the limit.** If you need two values, either pass one string and split it, the way `pluralize:"y,ies"` does, or use a custom tag instead of a filter. - **Keep filters presentational.** A filter that queries the database runs once per use in a loop; compute such values in the view instead. - **Test the function directly.** A filter is a function, so `money(Decimal("5"), "USD")` can be unit-tested without rendering a template, plus one render test for the `{% load %}` wiring. ## Testing a filter Because a filter is an ordinary function, most of its tests need no template at all: ```python from decimal import Decimal from django.test import SimpleTestCase from billing.templatetags.billing_extras import money class MoneyFilterTests(SimpleTestCase): def test_formats_two_places(self): self.assertEqual(money(Decimal("1234.5"), "USD"), "USD 1,234.50") def test_bad_input_is_blank(self): self.assertEqual(money(None), "") ``` `SimpleTestCase` is enough because nothing touches the database. Add one test that renders a template containing `{% load billing_extras %}` so a broken package layout or a missing `INSTALLED_APPS` entry fails in CI rather than on a page. ## Alternatives to `{% load %}` A library can also be registered through the `libraries` option of the `DjangoTemplates` backend under a different label, or added to the `builtins` option so every template gets it without `{% load %}`. Both are engine configuration rather than filter authoring; the explicit `{% load %}` keeps each template's dependencies visible.

  • Why does {% load billing_extras %} fail with 'is not a registered tag library' after you create the file?
    Usually one of three causes: the app is not in `INSTALLED_APPS`, the `templatetags` directory lacks `__init__.py`, or the development server was not restarted after the package was added. Also check that the name in `{% load %}` is the module name, not the app name.
  • How would you pass both a currency and a number of decimal places to one filter?
    A filter takes at most one argument, so either pass one string and split it inside the filter, for example `money:"USD,0"`, the same convention `pluralize` and `yesno` use, or write a custom tag, which accepts any number of arguments. Splitting keeps call sites short; a tag is clearer when the parameters grow.

saying these in an interview costs you the question

  • {% load %} takes the app name rather than the module name
  • Custom filters can live in any module as long as they are imported somewhere
  • A filter should raise ValueError on bad input so the template shows it
  • A filter can accept several arguments separated by commas
  • The autoreloader picks up a brand-new templatetags package without a restart