When writing a custom Django template filter, what do the is_safe, needs_autoescape and expects_localtime flags do, and when do you need each?
answer
- safe in, safe out
- the filter learns the escaping mode
- HTML you add yourself
- aware datetimes converted first
basics
~20 sis_safe=True keeps a safe input's result marked safe, for filters that add no HTML characters. needs_autoescape=True passes the current autoescape mode so a filter that builds HTML can escape its inputs. expects_localtime=True converts aware datetimes to the current time zone first.
solid answer
~40 sDjango applies the three flags in `FilterExpression.resolve`. With `is_safe=True`, if the input was a `SafeData` string, the result is marked safe again; plain inputs stay plain and get autoescaped on output. It suits filters that add no `<`, `>`, `&` or quotes, and is wrong for filters that return booleans or numbers, whose result gets turned into a string. `needs_autoescape=True` makes Django pass an `autoescape` keyword reflecting `{% autoescape %}`; a filter that builds markup, like a money filter wrapping the amount in `<span>`, should run every input through `conditional_escape` when it is true and return `mark_safe(...)`. `expects_localtime=True` converts an aware datetime to the current time zone before the filter runs; without it a filter sees the value in UTC and can show the wrong date.
code
python · 12 linesfrom django import template
from django.utils import timezone
register = template.Library()
@register.filter(expects_localtime=True)
def overdue(value):
try:
return value.date() < timezone.localdate()
except AttributeError:
return Falsego deeper
Know that custom filters that return HTML need special care, and that Django offers flags for escaping and for time zones when you register a filter.
Explain each flag's mechanism: is_safe re-marks safe input, needs_autoescape passes the mode, expects_localtime converts aware datetimes, and when each is appropriate.
Review filters for XSS: every interpolated input escaped with conditional_escape before mark_safe, no is_safe on non-string results, and datetime filters that declare expects_localtime.
Set the policy for HTML-producing helpers: prefer format_html-style helpers and plain-text filters, require review of every mark_safe, and test filters under autoescape on and off.
## Why filters need flags A custom filter is just a function, but it sits in the middle of two things the template engine manages for you: **autoescaping**, which HTML-escapes every value on output unless it is marked safe, and **time zone conversion**, which shows aware datetimes in the current time zone. A plain function knows nothing about either. The three keyword flags on `register.filter()` tell the engine how to treat the filter's input and output. All three default to `False`. ## `is_safe`: safe in, safe out Many ordinary string operations turn a `SafeString` back into a plain `str`. With `is_safe=True`, Django repairs this after the call: **if the input was safe, the output is marked safe**; if the input was plain, the output stays plain and is escaped on output as usual. Use it when the filter **adds no HTML-significant characters** (`<`, `>`, `'`, `"`, `&`) that were not already there, for example a filter that appends a suffix. Two cautions from Django's documentation: - **Removing characters can be unsafe too.** Stripping `>` can turn `<a>` into `<a`, and stripping `;` can break `&`; such a filter should not claim `is_safe`. - **It stringifies.** When the input is safe, the flag passes the result through `mark_safe`, which converts it to a string, so a filter returning `False` could render the text `False`. Do not set it on filters that return booleans or numbers. ## `needs_autoescape`: filters that build HTML A filter that **introduces markup** must escape its inputs itself and then mark the result safe. It also has to respect the template's current escaping mode, which `{% autoescape off %}` can switch. With `needs_autoescape=True`, Django passes an extra keyword argument, `autoescape`, that is `True` when autoescaping is in effect. ```python from decimal import Decimal, InvalidOperation 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 money_html(value, currency="EUR", autoescape=True): try: amount = Decimal(value).quantize(Decimal("0.01")) except (InvalidOperation, TypeError, ValueError): return "" esc = conditional_escape if autoescape else (lambda s: s) return mark_safe( f'<span class="currency">{esc(currency)}</span> {esc(f"{amount:,}")}' ) ``` Key points: 1. **Escape every input you interpolate**, including the argument. In `{{ total|money_html:invoice.currency }}` the currency comes from the database, and if an editable field ever holds `<script>`, an unescaped `mark_safe` publishes it. 2. **Use `conditional_escape`**, which leaves already-safe strings untouched so values are not double-escaped. 3. **Default `autoescape=True`** in the signature, so calling the function from Python escapes by default. 4. **`is_safe` is irrelevant here**: the function returns a safe string itself, so the flag changes nothing either way. Literal string arguments written in the template, like `money_html:"EUR"`, reach the filter already marked safe; arguments that come from variables do not. Escaping both through `conditional_escape` handles either case. ## `expects_localtime`: datetimes in the reader's time zone With `USE_TZ = True`, model datetimes are **aware** and stored in UTC. When a template prints `{{ invoice.due_at }}`, the engine converts it to the current time zone. A custom filter, however, receives the raw value, so a filter comparing `value.date()` with today would use the UTC date. With `expects_localtime=True`, Django converts an aware datetime to the current time zone **before** calling the filter. The built-in `date` and `time` filters are registered this way. ## The flags compared | Flag | What Django does | Set it when | |---|---|---| | `is_safe=True` | re-marks the result safe if the input was safe | the filter adds no HTML-special characters | | `needs_autoescape=True` | passes `autoescape=` to the filter | the filter builds HTML and escapes inputs itself | | `expects_localtime=True` | converts aware datetimes to the current time zone first | the filter reads a datetime's date or hour | ## Review checklist - **Every `mark_safe` in a filter** has each interpolated input escaped, the argument included. - **No `is_safe=True`** on filters that return non-strings or remove characters. - **Datetime filters** declare `expects_localtime=True`, and a test renders one near midnight UTC. - **Arguments are inputs too.** Anything that reaches the filter through its argument, a context variable or a model field, is as untrusted as the value. - **Test both escaping modes.** Render the filter once normally and once inside `{% autoescape off %}` and assert that user text is escaped only in the first case. ## Why interviewers ask this The flags look like trivia, but each maps to a production failure: `is_safe` misuse produces escaped markup or stringified booleans, a missing `conditional_escape` in a `needs_autoescape` filter is a stored XSS hole, and a forgotten `expects_localtime` shows the wrong due date to readers whose local day differs from the UTC day. A candidate who can name the failure behind each flag has usually written and reviewed such filters.
- Why is marking a boolean-returning filter is_safe a mistake?When the input is a safe string, `is_safe` makes Django pass the result through `mark_safe`, which converts it to a string. A filter meant to return `False` then returns the non-empty string `"False"`, which is truthy, so `{% if value|myfilter %}` takes the wrong branch. Leave `is_safe` off filters whose result is not text.
- Does a filter marked needs_autoescape still need to call mark_safe?Yes. The flag only tells the filter which mode is in effect; it does not change the output. A filter that returns HTML must escape its inputs when `autoescape` is true and then return `mark_safe(result)`, otherwise the engine escapes the filter's own markup and the page shows literal `<span>` tags.
saying these in an interview costs you the question
- is_safe=True makes the filter's output safe even for plain input
- needs_autoescape escapes the filter's output automatically
- Only the value needs escaping, not the filter argument
- is_safe is harmless on a filter that returns a boolean
- Custom filters receive datetimes already converted to local time