skip to content

In Django templates, how do you apply, chain and pass an argument to a filter, as in {{ invoice.notes|truncatechars:80 }}?

level: juniorimportance: must knowfreq 58%

answer

  1. a pipe inside the braces
  2. read left to right
  3. a colon, then one value
  4. quote anything with spaces

basics

~20 s

A filter follows a pipe inside {{ }} and transforms the value before output. Filters chain left to right, each receiving the previous result, and each takes at most one argument after a colon: a literal, a number or a variable.

solid answer

~40 s

In the Django template language a filter is written `{{ value|name }}` or `{{ value|name:arg }}`. The engine resolves the variable, then calls each filter in turn, so `{{ invoice.notes|linebreaks|truncatechars_html:200 }}` means `truncatechars_html(linebreaks(notes), 200)`. Each filter accepts at most one argument, written straight after the colon: a quoted string such as `", "`, a number, or a variable like `invoice.currency`. The argument count is checked when the template compiles, so a wrong count raises `TemplateSyntaxError` before any request is served. Filters also work inside tags, for example `{% if lines|length > 10 %}`. Common built-ins include `default`, `date`, `length`, `join`, `truncatechars`, `linebreaks` and `pluralize`.

code

django · 6 lines
django
{# context: invoice, lines (list), tags (list) #}
<p>Issued {{ invoice.issued_on|date:"j M Y" }}</p>
<p>{{ lines|length }} line{{ lines|length|pluralize }}</p>
<p>{{ tags|join:", " }}</p>
<p>{{ invoice.po_number|default:"not supplied" }}</p>
<div>{{ invoice.notes|truncatechars:300|linebreaks }}</div>

go deeper

for a junior

Recall the pipe syntax, left-to-right chaining and the single argument after a colon, and be able to use default, length, join, date, truncatechars, linebreaks and pluralize on a real page.

for a middle

Explain that the argument may be a variable, that arity is checked at compile time, and why the order of a chain like truncatechars and linebreaks changes the output.

for a senior

Show judgement about what belongs in a filter chain versus the view, and about filters that return HTML, their escaping, and truncating markup safely.

for a principal

Frame filters as the template's presentation vocabulary: keep it small and predictable, push formatting policy such as currency and dates into shared filters rather than ad hoc chains.

## What a filter is In the **Django template language** (DTL), a **filter** is a function applied to a value at render time, written after a pipe character inside a variable tag: `{{ invoice.total|floatformat:2 }}`. The engine first resolves the variable from the context, then passes the result through each filter, and finally converts the last result to text for output. Filters are the DTL's way of doing small presentation transformations without putting Python code in the template. Filters are ordinary Python functions registered in a template library. Django ships the built-in ones in `django/template/defaultfilters.py`; they are available in every template without `{% load %}`. ## The syntax rules 1. **Pipe then name.** `{{ value|lower }}` calls the `lower` filter with `value`; the conventional style puts no spaces around the pipe. 2. **Chaining runs left to right.** `{{ value|a|b }}` means `b(a(value))`: each filter receives the output of the one before it. 3. **One argument at most.** An argument follows a colon with no space: `{{ value|truncatechars:80 }}`. The argument can be a quoted string (`{{ tags|join:", " }}`), a number, or a variable (`{{ total|floatformat:precision }}`). 4. **Quote anything with spaces.** Filter arguments that contain spaces must be quoted, or the parser cannot tell where the argument ends. 5. **Arity is checked at compile time.** Django compares the number of arguments you supplied with the filter function's signature when it parses the template and raises `TemplateSyntaxError` (for example "truncatechars requires 2 arguments, 1 provided") if they do not match. Filters also appear inside tags that accept expressions: `{% if lines|length > 10 %}` or `{% for line in lines|dictsort:"position" %}`. ## Seven built-ins interviewers mention | Filter | Example on an invoice page | What it does | |---|---|---| | `default` | `{{ invoice.po_number\|default:"none" }}` | uses the argument when the value is falsy | | `date` | `{{ invoice.issued_on\|date:"j M Y" }}` | formats a date or datetime with format characters, giving `5 Sep 2026` | | `length` | `{{ lines\|length }}` | returns `len(value)`, or `0` if the value has no length | | `join` | `{{ tags\|join:", " }}` | joins a list with the argument, escaping each item when autoescaping is on | | `truncatechars` | `{{ invoice.notes\|truncatechars:80 }}` | cuts to at most 80 characters, the trailing `…` included | | `linebreaks` | `{{ invoice.notes\|linebreaks }}` | turns single newlines into `<br>` and blank lines into paragraphs | | `pluralize` | `{{ n }} entr{{ n\|pluralize:"y,ies" }}` | returns a singular or plural suffix, `s` by default | A few details worth knowing: - **`truncatechars` counts the ellipsis.** `{{ "Joel is a slug"|truncatechars:7 }}` renders `Joel i…`, seven characters in total. - **`pluralize` accepts one argument that it splits itself.** `"y,ies"` is a single string that the filter splits on the comma into singular and plural suffixes. The same trick is how a filter that needs two values can work around the one-argument rule. - **`linebreaks` and `join` return safe HTML** but escape their input first when autoescaping is on, so user text cannot inject markup through them. - **`date` without an argument** falls back to the configured default date format; passing an explicit format string makes the output independent of that setting. ## How filters interact with the rest of the template - **Invalid input.** If the variable does not exist, it resolves to an empty string by default and the filters still run on that, which is why `|default` can supply a fallback for a missing name. - **Escaping happens after the chain.** The final value is autoescaped on output unless a filter returned a string already marked safe. - **Exceptions surface.** The template language has no exception handling, so a filter that raises produces a server error. Built-in filters generally fail quietly instead: `length` returns `0` for `None`, and `truncatechars` returns the value unchanged when its argument is not a number. ## A worked example ```django <h1>Invoice {{ invoice.number }}</h1> <p>Issued {{ invoice.issued_on|date:"j M Y" }}, PO {{ invoice.po_number|default:"not supplied" }}</p> <p>{{ lines|length }} line{{ lines|length|pluralize }}</p> <p>Tags: {{ tags|join:", " }}</p> <div class="notes">{{ invoice.notes|truncatechars:300|linebreaks }}</div> ``` The last line shows why **order matters**: truncating first and then converting newlines keeps the markup valid, while `linebreaks|truncatechars:300` could cut through a `<p>` tag; for already-built HTML Django provides `truncatechars_html`.

  • Why can {{ total|floatformat:precision }} use a variable as the argument?
    A filter argument is parsed like a variable unless it is quoted or numeric. `precision` is resolved from the context at render time and passed as the argument, so a view can choose the number of decimal places per invoice. A quoted `"precision"` would instead pass the literal string.
  • When is a filter's argument count checked?
    At compile time. When Django parses `{{ value|truncatechars }}`, it inspects the filter function's signature, sees that a required argument is missing, and raises `TemplateSyntaxError`. The template never renders, so the mistake is caught on the first load rather than for some requests only.

A filter chain is an assembly line: the resolved value goes in at the left, each station reshapes what it receives and passes it on, and the display gets only what leaves the last station. Swap two stations and you get a different product.

saying these in an interview costs you the question

  • Filters are applied right to left, like nested function calls read inside out
  • A filter can take several comma-separated arguments like a Python call
  • truncatechars:80 returns 80 characters plus an ellipsis
  • Filters only work inside {{ }}, never inside tags such as if
  • A wrong argument count only fails when that line renders