When should you write a custom Django template tag instead of a filter, and how do simple_tag and inclusion_tag differ?
answer
- one value versus many inputs
- returns a string or a variable
- renders its own template
- wraps a block since 5.2
basics
~20 sWrite a tag when you need several arguments, the context, a new variable or a rendered fragment; a filter only transforms one value with one argument. simple_tag returns a value; inclusion_tag returns a dict that renders its own template.
solid answer
~40 sA filter is right when you transform a single value with at most one argument. Reach for a custom tag when you need **several arguments**, **access to the context**, **to store a result under a name**, or **to render a reusable fragment**. `@register.simple_tag` wraps a function taking any number of arguments; its return value is printed, and, when autoescaping is on, passed through `conditional_escape`, so build HTML with `format_html`. With `{% tag args as name %}` the result is stored in a variable instead. `@register.inclusion_tag("articles/related_box.html")` wraps a function that returns a **dictionary**; Django renders that template with the dictionary as its context, the classic way to build a "related articles" box. Django 5.2 added `@register.simple_block_tag`, which receives the rendered content between `{% tag %}` and `{% endtag %}`.
code
python · 9 linesfrom django import template
from django.utils.html import format_html
register = template.Library()
@register.simple_block_tag
def callout(content, level="info"):
return format_html('<div class="callout callout-{}">{}</div>', level, content)go deeper
Know that filters transform one value, tags do more, and that simple_tag returns a value while inclusion_tag renders a small template from a dictionary.
Explain the escaping of simple_tag output, the as variant, the fresh context of inclusion tags, and when simple_block_tag from Django 5.2 fits.
Choose the lightest tool, keep queries in tags visible and bounded, build HTML with format_html, and document which context a tag relies on.
Curate a small shared tag library as the project's component vocabulary, with clear ownership and tests, instead of each app inventing its own widgets.
## Filters versus tags Both live in a **template tag library**: a module in an app's `templatetags` package with `register = template.Library()`, loaded with `{% load %}`. They answer different needs: | Need | Filter | `simple_tag` | `inclusion_tag` | `simple_block_tag` (5.2+) | |---|---|---|---|---| | Transform one value | yes | possible | no | no | | More than one argument | no, at most one | any number | any number | any number | | Read the template context | no | `takes_context=True` | `takes_context=True` | `takes_context=True` | | Store the result in a variable | no | `as name` | no | `as name` | | Render a reusable template fragment | no | no | yes | no | | Receive a block of template content | no | no | no | yes, as `content` | Rule of thumb: **a filter for formatting one value, a `simple_tag` for computing something from several inputs, an `inclusion_tag` for a reusable chunk of markup**. ## `simple_tag` ```python from django import template register = template.Library() @register.simple_tag def reading_time(word_count, words_per_minute=200): minutes = max(1, round(word_count / words_per_minute)) return f"{minutes} min read" ``` ```django {% load article_extras %} {% reading_time article.word_count %} {% reading_time article.word_count 250 as rt %}<span>{{ rt }}</span> ``` Key behaviours: 1. **Arguments are resolved for you.** Quoted literals arrive as strings, unquoted names as their current values, and argument counts are checked when the template compiles. 2. **Output is escaped.** If autoescaping is on, the return value goes through `conditional_escape`. A tag that returns HTML should build it with `format_html`, which escapes the inputs and marks the result safe. 3. **`as name` stores instead of printing.** The value becomes a context variable for later use. ## `inclusion_tag`: the related-articles box An inclusion tag pairs a function with a template. The function returns a **dictionary**, and Django renders the template with it: ```python from django import template from articles.models import Article register = template.Library() @register.inclusion_tag("articles/related_box.html") def related_articles(article, limit=3): related = ( Article.objects.filter(topic=article.topic, is_published=True) .exclude(pk=article.pk) .order_by("-published_at")[:limit] ) return {"related": related, "heading": "Related articles"} ``` ```django {# articles/related_box.html #} <aside class="related"> <h3>{{ heading }}</h3> <ul> {% for item in related %} <li><a href="{{ item.get_absolute_url }}">{{ item.title }}</a></li> {% empty %} <li>Nothing related yet.</li> {% endfor %} </ul> </aside> ``` Any page can then write `{% related_articles article limit=5 %}`. The box's markup lives in one template, its query in one function, and every page that shows it stays consistent. Worth knowing: - **The fragment gets a fresh context** made from the returned dictionary, not the page's variables. - **The template is loaded once per render**, then reused if the tag appears inside a loop. - **Each call runs its function**, so the query runs once per use. ## `simple_block_tag` (Django 5.2) Added in **Django 5.2**, `@register.simple_block_tag` registers a tag with an end tag, `{% callout %}...{% endcallout %}` by default. The function's first parameter must be named `content` and receives the **rendered** block, already escaped; the return value is escaped like a `simple_tag`'s. It suits wrappers such as panels or callouts whose inner markup is written in the template. ## Testing custom tags Tags are easy to test at two levels: 1. **Call the function directly.** `reading_time(420)` returns `"2 min read"`; `related_articles(article)` returns a dictionary whose `related` QuerySet you can assert on. 2. **Render a small template.** `Template("{% load article_extras %}{% related_articles a %}").render(Context({"a": article}))` proves the library loads, the arguments parse, and the fragment template exists. The second test catches the mistakes the first cannot: a missing `__init__.py` in `templatetags`, a typo in the fragment's path, or an argument the template cannot resolve. ## Choosing in practice - **Formatting a price or a date**: a filter. - **"How long ago", reading time, a computed label from several fields**: a `simple_tag`. - **A widget with its own markup**, such as a related-articles box, pagination bar or user badge: an `inclusion_tag`. - **A wrapper around arbitrary template content**: `simple_block_tag`, or `{% include %}` with parameters when no Python logic is needed. Custom tags written with `@register.tag` and a hand-written parser and `Node` class remain available for true control-flow tags, but `simple_tag`, `inclusion_tag` and `simple_block_tag` cover almost every application need.
- Why does a simple_tag that returns an HTML string show escaped tags on the page?With autoescaping on, `simple_tag` passes its return value through `conditional_escape`, so a plain string containing `<span>` is escaped. Build the markup with `format_html`, which escapes the interpolated values and returns a safe string, and the markup renders as HTML without opening an injection hole.
- How does an inclusion tag's template see the page's variables, such as request?It does not by default: the fragment renders with a new context built from the returned dictionary, with only `csrf_token` copied across. To use page variables, register the tag with `takes_context=True`, accept `context` as the first parameter, and copy what you need into the returned dictionary, or pass values as explicit arguments.
saying these in an interview costs you the question
- A filter can take as many arguments as a tag
- simple_tag output is never escaped
- An inclusion tag's function returns a rendered HTML string
- An inclusion tag's template sees every variable of the page
- simple_block_tag receives the raw, unrendered template source