Django: launch pdb in templates with a custom {% breakpoint %} tag

Artist’s depiction of an early debugging tool.

In my recent Boost Your Django DX update, I added a new chapter on debuggers. Here’s an extra technique I didn’t finish in time for the update, but I will include it in the next one.

Django templates can be hard to debug, especially to figure out which variables are available after several levels of {% extends %} or {% include %} tags. The template engine doesn’t provide a built-in way tag to open the debugger, but adding one is not much work.

Below is a custom template tag that starts debugging with breakpoint(). Find this file in resources.zip as debuggers/party-central/example/templatetags/debugging.py.

from django import template

register = template.Library()


@register.simple_tag(name="breakpoint", takes_context=True)
def breakpoint_tag(context):
    """
    Launch Python’s debugger in templates.

    See: https://adamj.eu/tech/2024/11/28/django-template-breakpoint/
    """
    exec("breakpoint()", {}, context.flatten())

The tag uses exec() to populate the debugger’s local variables with all variables from the current context.

To set up this tag for convenient use:

  1. Copy the file as a template tag library called debugging.py.

    This should be in an app’s templatetags directory, as described in Django’s custom template tag tutorial. For example, if you have an app called core, copy this code to core/templatetags/debugging.py.

  2. Add the debugging library to the builtins option in TEMPLATES:

    TEMPLATES = [
        {
            "BACKEND": "django.template.backends.django.DjangoTemplates",
            "DIRS": [BASE_DIR / "example" / "templates"],
            "APP_DIRS": True,
            "OPTIONS": {
                "builtins": [
                    "example.templatetags.debugging",
                ],
                "context_processors": ...,
            },
        }
    ]
    

    This makes the library always available, with no need for a {% load debugging %} tag.

Now, you can invoke the tag with:

{% breakpoint %}

Insert at the point you want to debug.

When the template is rendered, the debugger will open like:

> <string>(1)<module>()
(Pdb)

The filename is listed as <string> due to the use of exec().

The debugger’s local variables are populated with the template context:

(Pdb) pp locals().keys()
dict_keys(['True', 'False', 'None', 'csrf_token', 'request', 'animals'])

Note that True, False, and None are included, a template engine quirk. Interact with variables as usual:

(Pdb) animals.count()
10

Running c (continue) resumes the template rendering:

(Pdb) c

If you add {% breakpoint %} to a template that is rendered many times in a loop, use q / Ctrl-D to exit and stop rendering.

The stack trace for template rendering is normally quite deep, with many internal function calls in django/template. You can see this as usual you can see with w (where):

(Pdb) w
  ...
  /.../example/views.py(13)index()
-> return render(
  /.../django/shortcuts.py(25)render()
-> content = loader.render_to_string(template_name, context, request, using=using)
  /.../django/template/loader.py(62)render_to_string()
-> return template.render(context, request)
  /.../django/template/backends/django.py(107)render()
...
  /.../django/template/library.py(237)render()
-> output = self.func(*resolved_args, **resolved_kwargs)
  /.../example/templatetags/debugging.py(8)breakpoint_tag()
-> exec("breakpoint()", {}, context.flatten())
> <string>(1)<module>()

So, if you need to jump to the code that started the template render, be prepared to run u (up) a bunch of times.

Fin

May your template debugging sessions be rare and swift,

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: