Django Module 2 – URLs, views, templates and static files
Template partials and custom template tags
Reuse a fragment with Django 6.0's partialdef and partial, render it alone by name, and write custom filters, simple tags and inclusion tags safely.
What you will learn
- Define and reuse named fragments with partialdef and partial
- Render a single fragment by name with 'template.html#name' from a view or an include
- Write a custom filter, a simple tag and an inclusion tag in a tag library
- Build HTML in Python with format_html() and conditional_escape() instead of mark_safe()
Before you start
What you’ll learn · Before you start
What you will learn
- Define and reuse named fragments with partialdef and partial
- Render a single fragment by name with 'template.html#name' from a view or an include
- Write a custom filter, a simple tag and an inclusion tag in a tag library
- Build HTML in Python with format_html() and conditional_escape() instead of mark_safe()
Before you start
On this page
The notice card of Nivas appears in 3 places: in the list, on each notice’s own page, and alone, as the answer to a script that adds one card to a page that is already open. Copy its markup three times and the copies drift apart with the first change; Django 6.0’s template partials give it one definition that all three render.
This lesson defines the card once with {% partialdef %}, renders it in a loop with {% partial %} and alone with
'notice_list.html#notice-card'. Then it writes the template code Nivas needs in Python: a filter that shows paise as
rupees, a simple tag and an inclusion tag, all of them escaping what residents typed, and you will see why
format_html() keeps that true where mark_safe() does not.
A partial names a fragment once and renders it where you need it
A template partial: A named fragment of a template, defined with {% partialdef name %} … {% endpartialdef %} and rendered with {% partial name %}, or loaded on its own as 'file.html#name'. New in Django 6.0. is a piece of a template with a name.
{% partialdef notice-card %} … {% endpartialdef %} defines it, and the definition alone renders nothing.
{% partial notice-card %} renders it, as often as you like, each time with the context at that point. Step through
the card of Nivas and the three places that render it:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Defined once. notice_list.html defines the card between partialdef notice-card and endpartialdef: a list item with the title, the resident badge and the fee. The definition alone renders nothing.
- Used in the loop. In the list's for loop, {% partial notice-card %} renders the card once for each notice, with the loop's notice in the context. The recorded list shows three cards.
- Included by another page. notice_detail.html includes notices/notice_list.html#notice-card. The same card appears in the page of notice 9, with that page's notice.
- Rendered alone. The notice_card view renders notices/notice_list.html#notice-card itself. The response is the li element and nothing else, the fragment a script such as htmx swaps into a page.
- One edit, three places. A change to the card's markup, between partialdef and endpartialdef, reaches the list, the notice's page and the fragment at once, because none of them has a copy of it.
The list template defines the card at the top and renders it in its loop; the detail page and the card view, in the next section, use the same definition:
nivas/notices/templates/notices/notice_list.html
{% extends "base.html" %}
{% load nivas_extras %}
{% partialdef notice-card %}
<li id="notice-{{ notice.pk }}">
<h2>{{ notice.title }}</h2>
{% resident_badge notice.posted_by notice.flat %}
{% if notice.fee_paise %}<p>Fee: {{ notice.fee_paise|paise }}</p>{% endif %}
</li>
{% endpartialdef %}
{% block title %}Notices · {{ block.super }}{% endblock %}
{% block content %}
<h1>Notices</h1>
{% fee_summary page_obj %}
<ul id="notices">
{% for notice in page_obj %}
{% partial notice-card %}
{% empty %}
<li>No notices match these filters.</li>
{% endfor %}
</ul>
{% endblock %} nivas/notices/templates/notices/notice_detail.html
{% extends "base.html" %}
{% block title %}{{ notice.title }} · {{ block.super }}{% endblock %}
{% block content %}
<h1>Notice {{ notice.pk }}</h1>
<ul>
{% include "notices/notice_list.html#notice-card" %}
</ul>
<p>{{ notice.body|linebreaksbr }}</p>
{% endblock %} Because a partial renders with the context it meets, the card inside the loop sees each pass’s notice, as a fragment
written out in the loop would. The definition can sit outside every block, as here: it only names the fragment.
There is one argument to know before you use it: {% partialdef card inline %} defines the partial and renders it
where it stands. This program shows what that does outside a loop:
"""partialdef … inline renders the fragment where it is defined as well. Outside the loop that means an extra,
empty item."""
import django
from django.conf import settings
settings.configure(TEMPLATES=[{'BACKEND': 'django.template.backends.django.DjangoTemplates'}])
django.setup()
from django.template import Context, Template # noqa: E402
LOOP = '<ul>{% for notice in notices %}{% partial card %}{% endfor %}</ul>'
DEFINED = '{% partialdef card %}<li>{{ notice }}</li>{% endpartialdef %}'
INLINE = '{% partialdef card inline %}<li>{{ notice }}</li>{% endpartialdef %}'
context = Context({'notices': ['Water tank cleaning', 'Lift repair']})
print('partialdef card, then the loop:')
print(' ', Template(DEFINED + LOOP).render(context))
print('partialdef card inline, then the loop:')
print(' ', Template(INLINE + LOOP).render(context))
print('partialdef card inline, inside the loop:')
inside = '<ul>{% for notice in notices %}' + INLINE + '{% endfor %}</ul>'
print(' ', Template(inside).render(context)) Output
partialdef card, then the loop: <ul><li>Water tank cleaning</li><li>Lift repair</li></ul> partialdef card inline, then the loop: <li></li><ul><li>Water tank cleaning</li><li>Lift repair</li></ul> partialdef card inline, inside the loop: <ul><li>Water tank cleaning</li><li>Lift repair</li></ul>
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inline_partial.py
Without inline, the list held exactly two items. With inline above the loop, the partial also rendered at its
definition, where no notice exists yet, and an empty <li></li> appeared before the list. Inside the loop, where the
fragment belongs anyway, inline defines and renders it in one go.
inline on a partial defined away from where it is used
{% partialdef card inline %} at the top of a template renders the card there too, with whatever context the top of
the template has. Leave out inline when the definition is not where the fragment belongs.
A partial can be rendered on its own, as a page’s fragment
A partial also has a template name of its own: the file’s name, #, and the partial’s name. Everything that loads a
template by name accepts it: render(), get_template() and {% include %}. So the detail page includes
notices/notice_list.html#notice-card, and a view can return the card and nothing else:
import csv
import io
from django.core.paginator import Paginator
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_http_methods, require_safe
from .models import Notice
CATEGORIES = ['water', 'lift', 'parking', 'security']
@require_safe
def notice_list(request):
# ?category=water&category=lift: getlist() keeps every value; unknown categories are ignored.
chosen = [c for c in request.GET.getlist('category') if c in CATEGORIES]
notices = Notice.objects.order_by('pk')
if chosen:
notices = notices.filter(category__in=chosen)
page_obj = Paginator(notices, 10).get_page(request.GET.get('page')) # 10 notices a page; a bad page gives page 1
return render(request, 'notices/notice_list.html', {'page_obj': page_obj, 'chosen': chosen})
@require_safe
def notice_detail(request, pk):
notice = get_object_or_404(Notice, pk=pk)
return render(request, 'notices/notice_detail.html', {'notice': notice})
@require_safe
def notice_card(request, pk):
# One card, rendered from the partial of the list page: the response a script such as htmx swaps into the page.
notice = get_object_or_404(Notice, pk=pk)
return render(request, 'notices/notice_list.html#notice-card', {'notice': notice})
@require_http_methods(['GET', 'POST'])
def notice_create(request):
if request.method == 'POST':
title = request.POST.get('title', '').strip()
category = request.POST.get('category', '')
if title and category in CATEGORIES:
notice = Notice.objects.create(title=title, category=category)
return redirect('notices:detail', pk=notice.pk) # Post/Redirect/Get
context = {'categories': CATEGORIES, 'error': 'Give a title and one of the categories.'}
return render(request, 'notices/form.html', context, status=422)
return render(request, 'notices/form.html', {'categories': CATEGORIES})
@require_safe
def notice_export(request):
rows = io.StringIO()
writer = csv.writer(rows)
writer.writerow(['id', 'title', 'category'])
for notice in Notice.objects.order_by('pk'):
writer.writerow([notice.pk, notice.title, notice.category])
return FileResponse(io.BytesIO(rows.getvalue().encode()), as_attachment=True, filename='notices.csv')
def flat_notices(request, code):
return HttpResponse(f'Notices for flat {code}, block {code.block}') Version note
partialdef, partial and the #name form are new in Django 6.0. A project on 5.2 LTS gets the same tags from the
django-template-partials package, and the 6.0 release notes link to its guide for moving to the built-in tags.
This program requests the list, the page of notice 9 and the card of notice 9 through the test client, with three notices in a test database:
"""One card, three uses: the notices list, a notice's own page and the card alone, all from one partial.
Run it next to manage.py. It makes a test database in memory with three notices and requests the three pages through
the test client, as a browser and a script such as htmx would.
"""
import logging
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
import django # noqa: E402
django.setup()
from django.test import Client # noqa: E402
from django.test.utils import setup_databases, setup_test_environment # noqa: E402
setup_test_environment()
setup_databases(verbosity=0, interactive=False, aliases={'default'})
logging.getLogger('django.request').setLevel(logging.CRITICAL)
from notices.models import Notice # noqa: E402
Notice.objects.create(pk=7, title='Water tank cleaning on Sunday', category='water', posted_by='Asha', flat='B-1203')
Notice.objects.create(pk=8, title='Clubhouse booking for Diwali', category='security', posted_by='Ravi', flat='A-101',
fee_paise=150000)
Notice.objects.create(pk=9, title='Lift annual maintenance', category='lift', posted_by='<i>Office</i>',
fee_paise=1234567, body='Paid in two parts.\nThe second in March.')
client = Client()
for url in ['/notices/', '/notices/9/', '/notices/9/card/']:
response = client.get(url)
print(f'GET {url} -> {response.status_code}')
keep = False
for line in response.text.splitlines():
line = line.strip()
keep = keep or line.startswith(('<h1', '<li', '<p')) # the page's own content, after the layout's head
if keep and line and line not in {'</main>', '</body>', '</html>'}:
print(' ' + line) Output
GET /notices/ -> 200
<h1>Notices</h1>
<p class="fees">2 notices with a fee, ₹13,845.67 in all</p>
<ul id="notices">
<li id="notice-7">
<h2>Water tank cleaning on Sunday</h2>
<span class="resident">Asha <small>B-1203</small></span>
</li>
<li id="notice-8">
<h2>Clubhouse booking for Diwali</h2>
<span class="resident">Ravi <small>A-101</small></span>
<p>Fee: ₹1,500.00</p>
</li>
<li id="notice-9">
<h2>Lift annual maintenance</h2>
<span class="resident"><i>Office</i></span>
<p>Fee: ₹12,345.67</p>
</li>
</ul>
GET /notices/9/ -> 200
<h1>Notice 9</h1>
<ul>
<li id="notice-9">
<h2>Lift annual maintenance</h2>
<span class="resident"><i>Office</i></span>
<p>Fee: ₹12,345.67</p>
</li>
</ul>
<p>Paid in two parts.<br>The second in March.</p>
GET /notices/9/card/ -> 200
<li id="notice-9">
<h2>Lift annual maintenance</h2>
<span class="resident"><i>Office</i></span>
<p>Fee: ₹12,345.67</p>
</li>
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 render_cards.py
The three responses render one definition. In the list, each card got its own notice from the loop; the page of
notice 9 included the card inside its own <ul>; and GET /notices/9/card/ answered with the <li> alone, without
base.html, without a <title>. That last shape is what a script needs to add or replace one card in a page that is
already open; htmx, which the module on front ends uses, works with fragments like it. In the 2026 Django Developers
Survey, 34 % of respondents used htmx.
Key idea
A partial is written once and rendered by name: with {% partial %} inside its own template, and as
file.html#name from a view or an include elsewhere.
Checkpoint
Check: Defining and using a partial · Rendering a partial on its own
A custom filter is a Python function in a tag library
The card shows a notice’s fee, and Nivas stores money as whole paise, the hundredth part of a rupee, so that no
floating-point rounding ever touches a bill. Formatting that number belongs in a filter. Custom filters and tags live
in a tag library: A module in the templatetags package of an installed app, with a module-level register = template.Library(). Its filters and tags are loaded into a template by the module's name, as in {% load nivas_extras %}.: a module in the templatetags package of an installed app, with an
__init__.py, and a module-level register = template.Library(). Each template that uses it starts with
{% load nivas_extras %}, the module’s name. Step through what happens between the load and the text on the page:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The template asks for a library. The card's template starts with {% load nivas_extras %}, which asks for the tag library of that name. Libraries are named after their modules, not after their apps.
- The module is found. When the template engine starts, it imports the templatetags modules of every installed app, so notices/templatetags/nivas_extras.py is known as nivas_extras. That is why a new module needs a restart of the development server.
- The library holds the names. The module's register, a template.Library, holds every filter and tag its decorators registered: the filter paise, the simple tag resident_badge and the inclusion tag fee_summary.
- The filter is called. When the card renders {{ notice.fee_paise|paise }}, Django calls paise() with the value of notice.fee_paise, 1234567, the whole number of paise for notice 9.
- Its text goes to the page. The filter returns '₹12,345.67'. It is a plain string, so auto-escaping still applies to it on its way to the page; here there is nothing to escape.
- A bad value, no error. For None, a string or a float, paise() returns '' instead of raising, because an exception in a filter becomes a server error for the whole page.
The library of Nivas, with the filter and the two tags of the next section:
from django import template
from django.utils.html import format_html
register = template.Library()
@register.filter
def paise(value):
"""An amount in paise as rupees: 1234567 gives '₹12,345.67'. Anything but a whole number gives ''."""
if isinstance(value, bool) or not isinstance(value, int):
return ''
sign = '-' if value < 0 else ''
rupees, rest = divmod(abs(value), 100)
return f'{sign}₹{rupees:,}.{rest:02d}'
@register.simple_tag
def resident_badge(name, flat=''):
"""Who posted a notice, and from which flat. format_html() escapes every value it puts into the markup."""
if not name:
return ''
if not flat:
return format_html('<span class="resident">{}</span>', name)
return format_html('<span class="resident">{} <small>{}</small></span>', name, flat)
@register.inclusion_tag('notices/fee_summary.html')
def fee_summary(notices):
"""How many of these notices ask for a fee, and the fees together."""
fees = [notice.fee_paise for notice in notices if notice.fee_paise]
return {'count': len(fees), 'total': sum(fees)} Paise and rupees
The paise filter turns 1234567 into ₹12,345.67: 12,345 rupees and 67 paise. This program calls it on amounts
and on values it must refuse, then from a template; the exercise of this lesson adds the Indian grouping of lakhs
and crores, ₹1,23,45,678.90.
"""The paise filter of Nivas, called as a function and from a template. Amounts are whole paise, so no rounding."""
import os
import sys
sys.path.insert(0, 'nivas') # the project folder, beside this file
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
import django # noqa: E402
django.setup()
from django.template import Context, Template # noqa: E402
from notices.templatetags.nivas_extras import paise # noqa: E402
for value in [1234567, 150000, 5, 0, -5050, None, '1234567', 12345.67, True]:
print(f'paise({value!r:>11}) -> {paise(value)!r}')
template = Template('{% load nivas_extras %}Fee: {{ fee|paise|default:"none" }}')
for fee in [150000, None]:
print(f'{{{{ fee|paise|default:"none" }}}} with fee={fee!r:>6} -> {template.render(Context({"fee": fee}))!r}') Output
paise( 1234567) -> '₹12,345.67'
paise( 150000) -> '₹1,500.00'
paise( 5) -> '₹0.05'
paise( 0) -> '₹0.00'
paise( -5050) -> '-₹50.50'
paise( None) -> ''
paise( '1234567') -> ''
paise( 12345.67) -> ''
paise( True) -> ''
{{ fee|paise|default:"none" }} with fee=150000 -> 'Fee: ₹1,500.00'
{{ fee|paise|default:"none" }} with fee= None -> 'Fee: none'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money.py
The whole numbers came out with two digits of paise and the minus sign first. None, a string, a float and True
gave an empty string, and the template’s default filter then printed none. That is deliberate: Django’s
documentation says an exception raised in a filter is exposed as a server error, so a filter returns a fallback for a
value it cannot format. True needs its own check, because bool is a subclass of int in Python.
Restart after adding a tag library
The development server does not pick up a new templatetags module by itself. After you add one, restart
runserver before {% load %} can find it.
Checkpoint
Check: Custom filters in a tag library
Simple tags return a value, inclusion tags render a template
A filter takes one value. A tag can take several arguments, and Django has two easy kinds. A
simple tag: A custom template tag made from a Python function with @register.simple_tag. It takes arguments and returns a value, which is escaped unless it is a safe string, such as the result of format_html()., @register.simple_tag, is a function whose return value is printed where the
tag stands: {% resident_badge notice.posted_by notice.flat %} in the card calls resident_badge() with the two values.
An inclusion tag: A custom template tag made with @register.inclusion_tag('file.html'). Its Python function returns a dictionary, and the tag renders that template with it as the context., @register.inclusion_tag('notices/fee_summary.html'), is a function
that returns a dictionary, and the tag renders that small template with it. {% fee_summary page_obj %} at the top of
the list adds up the fees of the notices in Python and leaves the sentence to the template:
{% load nivas_extras %}
{% if count %}<p class="fees">{{ count }} notice{{ count|pluralize }} with a fee, {{ total|paise }} in all</p>{% endif %} The list in the run above shows both: its sentence about two notices with a fee came from the inclusion tag, which
uses the paise filter inside its own template, and each <span class="resident"> came from the simple tag. Notice 9
was posted by <i>Office</i>, and its badge shows those characters as text, <i>Office</i>, because the
simple tag built its markup with format_html(), the subject of the next section.
Choosing between the five tools of this module comes down to what the piece is:
| The piece is | Use |
|---|---|
| A fragment of one template, used again in it or rendered alone | a partial |
| A fragment many templates share | its own file and {% include %} |
| A value changed for display | a filter |
| A value or a short snippet computed from arguments | a simple tag |
| Markup that needs data computed in Python first | an inclusion tag |
Escape every value when Python builds HTML
A simple tag that returns a plain string has it escaped, markup and all. To return markup, a tag must return a
safe string: A string marked as HTML that needs no more escaping, such as the result of format_html() or mark_safe(). Templates print it as it is, so it must never hold unescaped text from a user., and there are two ways to make one. mark_safe(text) marks a string as
trusted HTML and checks nothing. format_html(template, *values) escapes each value, puts it into the markup and marks
only the result safe. This program builds the same badge both ways, for a resident who typed a link into their name:
"""Building HTML in Python. The same badge two ways, for a name that a resident typed with markup in it."""
import django
from django.conf import settings
settings.configure()
django.setup()
from django.utils.html import conditional_escape, format_html, format_html_join # noqa: E402
from django.utils.safestring import mark_safe # noqa: E402
def badge_concatenated(name, flat): # never do this: the name goes into the markup unescaped
return mark_safe(f'<span class="resident">{name} <small>{flat}</small></span>')
def badge_formatted(name, flat):
return format_html('<span class="resident">{} <small>{}</small></span>', name, flat)
for name in ['Asha', 'Ravi <a href="https://pay.example/">pay here</a>']:
for build in [badge_concatenated, badge_formatted]:
html = build(name, 'B-1203')
live = '<a href' in html
print(f'{build.__name__}({name!r}):')
print(f' {html}')
print(f' the typed link is {"a live link: a hole" if live else "shown as text"}')
print('conditional_escape() escapes text once, and leaves safe strings alone:')
print(' ', conditional_escape('<b>'), '|', conditional_escape(conditional_escape('<b>')), '|',
conditional_escape(mark_safe('<b>')))
flats = [('B-1203', 'Asha'), ('A-101', 'Ravi & Meera')]
print('format_html_join() for a list:')
print(' ', format_html_join('', '<li>{} ({})</li>', flats)) Output
badge_concatenated('Asha'):
<span class="resident">Asha <small>B-1203</small></span>
the typed link is shown as text
badge_formatted('Asha'):
<span class="resident">Asha <small>B-1203</small></span>
the typed link is shown as text
badge_concatenated('Ravi <a href="https://pay.example/">pay here</a>'):
<span class="resident">Ravi <a href="https://pay.example/">pay here</a> <small>B-1203</small></span>
the typed link is a live link: a hole
badge_formatted('Ravi <a href="https://pay.example/">pay here</a>'):
<span class="resident">Ravi <a href="https://pay.example/">pay here</a> <small>B-1203</small></span>
the typed link is shown as text
conditional_escape() escapes text once, and leaves safe strings alone:
<b> | <b> | <b>
format_html_join() for a list:
<li>B-1203 (Asha)</li><li>A-101 (Ravi & Meera)</li>
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 badges.py
For Asha the two badges were the same. For the name with a link in it, the f-string and mark_safe() put a live link
into every page that shows the badge; format_html() escaped the name, and the link stayed text. Django’s
documentation strongly recommends format_html() over mark_safe() for building small snippets of HTML.
conditional_escape() escapes text once and leaves a safe string alone, as the second call in the run shows, and
format_html_join() builds a list of pieces, escaping each value, as Ravi & Meera shows.
Never pass user text through mark_safe
mark_safe(f'<span>{name}</span>'), mark_safe('<span>' + name + '</span>') and the same with % all put whatever
the user typed into your markup. Write format_html('<span>{}</span>', name), and review every mark_safe() in a
code base as a place where escaping is switched off.
Checkpoint
Check: Building HTML safely in Python · Simple tags and inclusion tags
Practise: rupees with Indian grouping
Lakh and crore
Amounts in rupees are usually grouped the Indian way: the last three digits, then groups of two, so twelve lakh
thirty-four thousand five hundred and sixty-seven rupees is written ₹12,34,567, and a crore has eight digits.
Write paise_to_rupees(value, indian_grouping=False), the function behind a filter like the lesson’s, with both kinds
of grouping and the same rule for values it cannot show. Its eight sample tests run in your browser and cover both
kinds of grouping, negative amounts and the values a filter must refuse:
Exercise · Medium · Python
Format paise as rupees, with Indian grouping
Nivas stores money as whole paise, the hundredth part of a rupee, so 1234567 is 12,345.67 rupees and nothing is lost to floating point. Write paise_to_rupees(value, indian_grouping=False), the function behind a template filter that shows such an amount:
- The result is the rupee sign, the rupees with a comma between groups of digits, a point and two digits of paise:
paise_to_rupees(1234567)is'₹12,345.67', andpaise_to_rupees(5)is'₹0.05'. - By default the digits are grouped in threes:
paise_to_rupees(123456789)is'₹1,234,567.89'. - With
indian_grouping=Truethey are grouped the Indian way, the last three digits and then groups of two (lakh and crore):paise_to_rupees(123456789, indian_grouping=True)is'₹12,34,567.89', and1234567890gives'₹1,23,45,678.90'. - A negative amount starts with the minus sign:
paise_to_rupees(-5050)is'-₹50.50'. - A template filter should not raise for a value it cannot show, so
None,TrueorFalse, afloatand a string all give''.
Starter code · rupees.py
def paise_to_rupees(value, indian_grouping=False):
"""An amount in whole paise as rupees, such as '₹12,345.67'; '' for a value that is not a whole number."""
return f'₹{value / 100}' The sample tests · test_rupees.py
from rupees import paise_to_rupees
def test_an_amount_with_paise():
assert paise_to_rupees(1234567) == '₹12,345.67'
def test_small_amounts_keep_two_digits_of_paise():
assert paise_to_rupees(5) == '₹0.05'
assert paise_to_rupees(0) == '₹0.00'
assert paise_to_rupees(100) == '₹1.00'
def test_groups_of_three_by_default():
assert paise_to_rupees(123456789) == '₹1,234,567.89'
assert paise_to_rupees(99999) == '₹999.99'
def test_indian_grouping_for_lakhs():
assert paise_to_rupees(123456789, indian_grouping=True) == '₹12,34,567.89'
assert paise_to_rupees(1234567, indian_grouping=True) == '₹12,345.67'
def test_indian_grouping_for_crores():
assert paise_to_rupees(1234567890, indian_grouping=True) == '₹1,23,45,678.90'
def test_amounts_under_a_thousand_rupees_have_no_comma():
assert paise_to_rupees(99999, indian_grouping=True) == '₹999.99'
assert paise_to_rupees(100000, indian_grouping=True) == '₹1,000.00'
def test_negative_amounts():
assert paise_to_rupees(-5050) == '-₹50.50'
assert paise_to_rupees(-12345678, indian_grouping=True) == '-₹1,23,456.78'
def test_values_that_are_not_whole_numbers_give_an_empty_string():
for value in [None, True, False, 12.5, '100']:
assert paise_to_rupees(value) == '', repr(value) A hint
divmod(abs(value), 100) splits an amount into rupees and paise, and f'{rest:02d}' writes the paise with two digits. For the Indian grouping, write the rupees as text and cut it from the right: the last three digits first, then two at a time. Check for bool before int, because True is an int in Python.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Fifteen questions follow on partials, fragments, filters, tags and safe HTML, chosen from where your answers so far were weakest, and every choice gives its reason:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to mid-level): Simple tag or inclusion tag: what does each return, and when do you pick which?
Both are custom template tags written as Python functions in a tag library. A simple tag returns a value, which is
printed where the tag stands and escaped unless it is a safe string, so it suits a computed value or a short snippet
built with format_html(). An inclusion tag returns a dictionary, and Django renders a template named in its
decorator with that dictionary as the context, so the markup stays in a template. Choose an inclusion tag when the
output is markup with conditions or loops, and a simple tag when it is a value.
Two design questions of this module start from these tags: one card used by three responses, and a review of a tag that builds HTML.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A notice card appears in the list page, on each notice's page, and as the response a script fetches to add one card to an open page. The team keeps three copies of its markup. How do you get to one copy, and how does one URL answer both a full page and a fragment?
-
Fresher to senior
In a code review you find a custom tag that returns mark_safe(f'<span class="badge">{name}</span>'), where name is a resident's display name. What is wrong, how do you fix it, and how do you prove the fix with a test?
Key takeaways
{% partialdef name %}defines a named fragment and{% partial name %}renders it with the current context;inlinealso renders it where it is defined, which outside a loop adds an extra, empty copy.'file.html#name'renders one partial alone fromrender(),get_template()or{% include %}, which gives a view a fragment response without a second copy of the markup; 5.2 LTS needs django-template-partials for it.- Custom filters live in a
templatetagsmodule of an installed app withregister = template.Library(), are loaded with{% load %}, and return a fallback such as''instead of raising. - A simple tag returns a value; an inclusion tag returns a dictionary that renders its own small template.
- Build HTML in Python with
format_html(),format_html_join()andconditional_escape(), which escape every value, never with an f-string insidemark_safe().
Terms in this lesson
- inclusion tag
- A custom template tag made with @register.inclusion_tag('file.html'). Its Python function returns a dictionary, and the tag renders that template with it as the context.
- safe string
- A string marked as HTML that needs no more escaping, such as the result of format_html() or mark_safe(). Templates print it as it is, so it must never hold unescaped text from a user.
- simple tag
- A custom template tag made from a Python function with @register.simple_tag. It takes arguments and returns a value, which is escaped unless it is a safe string, such as the result of format_html().
- tag library
- A module in the templatetags package of an installed app, with a module-level register = template.Library(). Its filters and tags are loaded into a template by the module's name, as in {% load nivas_extras %}.
- template partial
- A named fragment of a template, defined with {% partialdef name %} … {% endpartialdef %} and rendered with {% partial name %}, or loaded on its own as 'file.html#name'. New in Django 6.0.
References
- The Django template language: template partials (Django Software Foundation)
- Built-in template tags and filters (Django Software Foundation)
- How to create custom template tags and filters (Django Software Foundation)
- Django 6.0 release notes (Django Software Foundation)
- django-template-partials (supported Django releases) (django-template-partials (PyPI))
- Django utils: django.utils.html (Django Software Foundation)
- Cross Site Scripting Prevention Cheat Sheet (OWASP Foundation)
- Django Developers Survey 2026 results (Django Software Foundation and JetBrains)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress