Django Custom Template Tags and Filters: A Practical Guide (Django 5.x)

Blog / Django · November 18, 2023 · Updated June 10, 2026 · 10 min read
Django Custom Template Tags and Filters: A Practical Guide (Django 5.x)

Django ships with around two dozen built-in template tags and filters, but eventually you hit a wall: you need to format a value a specific way, render a reusable UI fragment, or pull data into a template that the view didn't pass down. That's when you write a custom template tag or filter.

Quick answer: Create a templatetags/ package inside a Django app, define your functions with @register.filter or @register.simple_tag, then {% load %} them in your template. The whole setup takes about five minutes once you know the moving parts.

  • Custom filter — transforms a single value: {{ value|my_filter }}. Use it for formatting and small per-value tweaks.
  • Simple tag — a Python function callable as {% my_tag arg %}; can read the template context and assign its result to a variable with as.
  • Inclusion tag — renders a small template fragment with its own context. Perfect for reusable widgets like nav menus or cards.
  • Advanced compilation tag — full control via a parser and Node subclass, for block tags like {% my_block %}...{% endmy_block %}.

This guide is current for Django 5.x on Python 3.12+. If you're upgrading from an older codebase, note that @register.assignment_tag was deprecated in Django 1.9 and removed in Django 2.0 — use simple_tag with as instead. We've maintained large Django codebases for 12+ years across 50+ projects, and the patterns below are the ones we reach for in production.

New to the template layer? Start with our intro to the Django template language and the Django template engine basics.

Setting up the templatetags package

Django only discovers custom tags and filters that live in a templatetags Python package inside an installed app. Three things must all be true:

  1. The app is listed in INSTALLED_APPS.
  2. The app contains a templatetags/ directory with an __init__.py file (it has to be a real package).
  3. Inside that package, a module defines register = template.Library() and decorates your functions.

Here's the layout for an app called blog:

myproject/
└── blog/
    ├── __init__.py
    ├── models.py
    ├── views.py
    └── templatetags/
        ├── __init__.py          # required — makes it a package
        └── blog_extras.py       # your tags and filters live here

The module name (blog_extras here) is what you reference in {% load %}, so keep it descriptive and unique across your project. Every tag/filter module starts the same way:

# blog/templatetags/blog_extras.py
from django import template

register = template.Library()

template.Library() is the registry Django uses to find your tags and filters. You attach functions to it with decorators like @register.filter and @register.simple_tag. To use them in a template, load the module by its file name:

{% load blog_extras %}

{# now your custom tags and filters are available below #}

After adding or renaming a templatetags module you must restart the development server — the autoreloader doesn't always pick up brand-new tag libraries. This is the single most common reason a tag "won't load."

Writing custom filters

A filter is a Python function that takes the value on its left ({{ value|my_filter }}) and returns the transformed value. It can take at most one extra argument, passed after a colon: {{ value|my_filter:"arg" }}.

A basic filter

# blog/templatetags/blog_extras.py
from django import template

register = template.Library()


@register.filter
def cut(value, arg):
    """Remove all occurrences of `arg` from the string."""
    return value.replace(arg, "")

Used in a template:

{% load blog_extras %}
{{ "hello world"|cut:" " }}   {# -> helloworld #}

Naming and string-only filters

By default a filter's function name is its template name. Override it with name=, and use the @stringfilter decorator when your filter only makes sense for strings — it converts the input to a string first and avoids AttributeError on non-string values:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()


@register.filter(name="shout")
@stringfilter
def to_upper(value):
    """Always-uppercase version of a string."""
    return value.upper()

Filters and auto-escaping

This is where correctness matters. Django auto-escapes template output by default. If your filter returns plain text, you don't need to do anything special. But if it returns HTML, you must opt into the escaping protocol correctly or you'll either double-escape your markup or open an XSS hole.

  • Set is_safe=True only when your filter never introduces unsafe characters (e.g. it just appends digits). It tells Django to preserve the "safe" status of an already-safe input.
  • Set needs_autoescape=True when your filter builds HTML and needs to know whether auto-escaping is on, so it can escape the interpolated parts itself.

The official pattern for an HTML-producing filter:

from django import template
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe

register = template.Library()


@register.filter(needs_autoescape=True)
def initial_letter(text, autoescape=True):
    """Wrap the first character in <strong>, escaping the rest safely."""
    first, other = text[0], text[1:]
    esc = conditional_escape if autoescape else (lambda x: x)
    result = "<strong>%s</strong>%s" % (esc(first), esc(other))
    return mark_safe(result)

conditional_escape escapes the user-supplied parts (so <script> can't slip through), and mark_safe then tells Django the final string is already safe so it isn't escaped again.

Avoid querying the database inside filters

A filter is evaluated on every render — sometimes inside a loop, once per row. Running a queryset inside a filter is a classic N+1 trap. Do the data work in your view or a model manager, then format with the filter. If you genuinely need data-fetching logic in the template layer, a simple_tag is the better tool, and even then you should batch the query in the view. See our notes on Django database access optimization.

Writing simple tags

A simple_tag is a Python function exposed as {% my_tag %}. Unlike filters, simple tags accept any number of positional and keyword arguments, can read the template context, and can assign their output to a variable.

A basic simple tag

from django import template
from django.utils import timezone

register = template.Library()


@register.simple_tag
def current_time(format_string):
    return timezone.now().strftime(format_string)
{% load blog_extras %}
{% current_time "%Y-%m-%d %H:%M" %}

Reading the context with takes_context=True

Pass takes_context=True and Django injects the current template context as the first argument, which must be named context. This lets a tag react to whatever the surrounding template knows — the logged-in user, the active request, and so on:

@register.simple_tag(takes_context=True)
def greeting(context, fallback="there"):
    user = context.get("user")
    name = user.get_full_name() if user and user.is_authenticated else fallback
    return f"Hello, {name}!"

Assigning the result with as

Any simple_tag can store its return value in a context variable instead of rendering it — this is the modern replacement for the removed assignment_tag:

{% greeting fallback="guest" as welcome %}
<h1>{{ welcome }}</h1>

Producing HTML safely from a tag

If a simple tag returns HTML, prefer format_html over mark_safe. format_html works like str.format but escapes every argument for you, so it's both convenient and XSS-safe by construction — you never hand-build a string and forget to escape a value:

from django.utils.html import format_html

register = template.Library()


@register.simple_tag
def badge(label, css_class="badge"):
    # Each {} argument is auto-escaped; the literal markup is trusted.
    return format_html('<span class="{}">{}</span>', css_class, label)

Reach for raw mark_safe only when you have a string you are 100% certain is already safe and contains no user input. As a rule: format_html for building HTML, mark_safe for trusting a static literal.

Writing inclusion tags

An inclusion tag renders a small template and returns the rendered output. It's the cleanest way to build reusable UI components — pagination controls, nav menus, comment widgets — that need their own template plus a bit of logic.

The function returns a dict, which becomes the context for the fragment template:

# blog/templatetags/blog_extras.py
from django import template

register = template.Library()


@register.inclusion_tag("blog/_tag_list.html")
def show_tags(post):
    return {"tags": post.tags.all()}
{# blog/templates/blog/_tag_list.html #}
<ul class="tag-list">
  {% for tag in tags %}
    <li>{{ tag.name }}</li>
  {% endfor %}
</ul>
{% load blog_extras %}
{% show_tags post %}

Inclusion tags also support takes_context=True. When set, the parent context is passed in as the first context argument, and you don't accept that argument from the template call:

@register.inclusion_tag("blog/_jump_link.html", takes_context=True)
def jump_link(context):
    return {
        "link": context["home_link"],
        "title": context["home_title"],
    }

Filter vs simple_tag vs inclusion_tag vs advanced tag

Type Returns Context access as assignment Best for Complexity
Filter Transformed value No No Formatting a single value ({{ x|filter }}) Low
simple_tag Any value (string/object) Yes (takes_context) Yes Logic + multiple args; computing a value Low–Medium
inclusion_tag Rendered template fragment Yes (takes_context) No Reusable UI components with their own template Medium
Advanced tag Custom Node output Yes (full control) Yes (DIY) Block tags, custom parsing ({% x %}...{% endx %}) High

Rule of thumb: start with a filter for value-formatting, a simple_tag for logic, and an inclusion_tag when there's a template fragment involved. Only drop to the advanced API when you truly need block-level parsing.

Advanced tags: the low-level compilation API

When you need a block tag — something like {% upper %}...{% endupper %}simple_tag isn't enough. You register a compilation function with @register.tag that parses the tag and returns a Node, whose render() method produces the output. This is the most powerful and most verbose option:

from django import template

register = template.Library()


class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist

    def render(self, context):
        # Render everything between the tags, then uppercase it.
        return self.nodelist.render(context).upper()


@register.tag(name="upper")
def do_upper(parser, token):
    # Parse until the matching {% endupper %}.
    nodelist = parser.parse(("endupper",))
    parser.delete_first_token()  # consume {% endupper %}
    return UpperNode(nodelist)
{% load blog_extras %}
{% upper %}
  this whole block will be uppercased
{% endupper %}

Most projects rarely need this; simple_tag and inclusion_tag cover the vast majority of real-world cases. Reach for the low-level API only for genuine block constructs or custom parsing.

Testing custom template tags and filters

Template tags are just Python functions, so the simplest tests call them directly. For end-to-end coverage, render a small template string with Template and Context and assert on the output — this also catches {% load %} and registration mistakes:

# blog/tests/test_templatetags.py
from django.template import Context, Template
from django.test import SimpleTestCase

from blog.templatetags.blog_extras import cut


class TemplateTagTests(SimpleTestCase):
    def test_cut_filter_directly(self):
        self.assertEqual(cut("a b c", " "), "abc")

    def test_badge_renders_and_escapes(self):
        tpl = Template(
            '{% load blog_extras %}'
            '{% badge label css_class="pill" %}'
        )
        out = tpl.render(Context({"label": "<x>"}))
        # format_html escapes the user value, proving it is XSS-safe.
        self.assertIn('class="pill"', out)
        self.assertIn("&lt;x&gt;", out)

SimpleTestCase is enough when your tags don't touch the database; switch to TestCase if they do. For more on structuring Django tests, see Django unit test cases with forms and views.

Where custom tags fit in a real project

Custom tags and filters keep presentation logic out of your views and out of your models — the template layer stays the right place to format and assemble output. Common production uses we see:

  • Rendering reusable components (breadcrumbs, pagination, rating stars) as inclusion tags.
  • Formatting domain-specific values (currency, status labels, relative dates) as filters.
  • Surfacing context-aware data — feature flags, the current site, active nav state — via takes_context simple tags.

If you're building or modernising a Django application and want a team that knows the template engine, the ORM, and the deployment story end to end, MicroPyramid has shipped 50+ projects over 12+ years of Django development and broader Python engineering.

Frequently Asked Questions

How do I create a custom template filter in Django?

Create a templatetags/ package inside an installed app (with an __init__.py), add a module such as blog_extras.py, and in it write register = template.Library() followed by a function decorated with @register.filter. The function takes the piped value plus at most one extra argument and returns the transformed value. Then {% load blog_extras %} in your template and use it as {{ value|your_filter }}. Restart the dev server after adding a new tag library.

What's the difference between a simple_tag and an inclusion_tag?

A simple_tag runs a Python function and returns a single value (a string or object) — use it for logic, computing values, and as assignment. An inclusion_tag renders a separate template fragment using a context dict your function returns — use it for reusable UI components that have their own markup. In short: simple_tag returns a value, inclusion_tag returns rendered HTML from a template.

Why isn't my custom template tag loading?

The four usual causes, in order: (1) the app isn't in INSTALLED_APPS; (2) the templatetags directory is missing its __init__.py, so it isn't a real package; (3) you didn't restart the development server after adding the new module — the autoreloader often misses brand-new tag libraries; or (4) the {% load %} name doesn't match the module's file name. Also confirm you actually called register = template.Library() and decorated the function.

How do I pass arguments to a custom template tag?

For a filter, pass one extra argument after a colon: {{ value|my_filter:"arg" }}. A simple_tag accepts any number of positional and keyword arguments: {% my_tag 123 "abc" warning=msg|lower %}. To capture the result instead of rendering it, append as var_name: {% my_tag arg as result %}, then use {{ result }} later in the template.

How do I safely output HTML from a custom template tag?

Prefer format_html from django.utils.html — it works like str.format but auto-escapes every argument, so user input can't inject markup. For an HTML-producing filter, set needs_autoescape=True, escape interpolated parts with conditional_escape, and wrap the final string in mark_safe. Use raw mark_safe only on a static literal you're certain contains no user data; never on a string you concatenated from inputs.

What replaced assignment_tag in modern Django?

@register.assignment_tag was deprecated in Django 1.9 and removed in Django 2.0. Use @register.simple_tag with the as keyword instead. Where you previously wrote {% my_function as var %} with an assignment tag, you now register the function as a simple tag and call it the same way: {% my_function as var %} — same template syntax, current decorator. This is the correct pattern for Django 5.x.

Share this article