Django ships no built-in filter for formatting Vietnamese currency, but you do not need to format in the view or the model. Create a custom template filter with @register.filter, load it with {% load %}, and use it right in the template as {{ price|vnd }}. Use this approach when the same formatting repeats across many templates and you want to keep display logic separate from business logic.

The problem

In a shop project, prices appear on the list page, the detail page, the cart, and the confirmation email. If each view formats the price itself with an f-string and passes the formatted string down to the template, you repeat the same snippet in many places. When the display convention changes, you must fix every view one by one and it is easy to miss one.

Another common mistake is formatting inside the model, for example adding a formatted_price() method. That mixes display logic into the data layer, bloats the model, and makes it harder to reuse.

The solution

Django lets you define your own filters inside an app’s templatetags package. Create shop/templatetags/shop_tags.py, and remember to also create an __init__.py file inside the templatetags folder, or Django will not find the module:

from django import template
register = template.Library()
@register.filter
def vnd(value):
try:
return f"{int(value):,} đ".replace(",", ".")
except (TypeError, ValueError):
return value
@register.filter
def shorten(value, length=50):
text = str(value)
return text if len(text) <= length else text[:length].rstrip() + "…"

In the template, load the library and use the filter just like a built-in Django filter:

{% load shop_tags %}
<p>Giá: {{ product.price|vnd }}</p>
<p>{{ product.description|shorten:60 }}</p>

The filter takes the value on the left of | as its first argument, and the parameter after the colon as its second argument. So shorten:60 is just a template-side call to shorten(value, 60). When the value is not a number, the vnd filter returns it unchanged instead of breaking the page, so the page never throws a 500 error on missing data.

Apply it to your project

Start with exactly one filter your project repeats most often — usually currency or date formatting — then expand gradually. Give filters short names that describe what they do, and add a template-render test to lock the formatting behavior in place. Once the team is used to it, gather shared filters into a dedicated utility app so every project can reuse them without copying files.

Related posts

Frequently asked questions

How is a filter different from a template tag?

A filter transforms one value and always appears after |, for example {{ price|vnd }}. A tag performs a more complex action, such as looping, branching, or rendering a whole template fragment, and is not limited to a single input value.

What happens if I forget __init__.py in templatetags?

Django will not recognize the filter module and the template will report “Invalid template library specified”. Always create __init__.py in both the app folder and the templatetags folder, then restart the development server after adding a new file.

Can a filter handle a None value?

Yes, but the filter must handle it itself. In the example above, vnd catches TypeError and ValueError and returns the original value, so the page does not throw a 500 error when data is missing.

Should all display logic be written as filters?

No. Filters suit small, side-effect-free transformations that do not query the database. Heavy logic, logic needing lots of data, or logic with side effects belongs in the view; the filter only handles the final presentation.

💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold the best ideas into the post.

Sources