skip to content

In Django's messages framework, why does messages.debug() show nothing by default, and how do MESSAGE_LEVEL, MESSAGE_TAGS and extra_tags work?

level: middleimportance: should knowfreq 36%

answer

  1. levels are plain integers
  2. a threshold at add time
  3. default recording level is INFO
  4. tags merge over defaults

basics

~10 s

Django records only messages at or above MESSAGE_LEVEL, which defaults to INFO (20), so DEBUG (10) messages are dropped when added. MESSAGE_TAGS overrides level-to-CSS tags, and extra_tags adds per-message tags to message.tags.

solid answer

~30 s

Levels are integers: `DEBUG` 10, `INFO` 20, `SUCCESS` 25, `WARNING` 30, `ERROR` 40. `BaseStorage.add()` ignores messages below the storage's level, which comes from `MESSAGE_LEVEL` or defaults to `INFO`, so `messages.debug()` is silently dropped at add time. Lower it in settings (`MESSAGE_LEVEL = message_constants.DEBUG`, importing `django.contrib.messages.constants` to avoid circular imports) or per request with `messages.set_level(request, messages.DEBUG)`. `message.tags` is `extra_tags` plus the level tag; `MESSAGE_TAGS` is merged over the default tags, so `{message_constants.ERROR: "danger"}` changes just one. Custom levels are just integers with an optional `MESSAGE_TAGS` entry.

code

python · 5 lines
python
# settings.py
from django.contrib.messages import constants as message_constants

MESSAGE_LEVEL = message_constants.DEBUG  # development only; default is INFO
MESSAGE_TAGS = {message_constants.ERROR: "danger"}  # merged over the defaults

go deeper

for a junior

Remember the five levels and that the default threshold is INFO, which is why messages.debug() shows nothing out of the box.

for a middle

Explain filtering at add time, MESSAGE_LEVEL versus set_level, how message.tags combines extra_tags with the level tag, and MESSAGE_TAGS merging.

for a senior

Map levels to the project's CSS once in MESSAGE_TAGS, use extra_tags for placement or grouping, and keep debug notices out of production by level.

for a principal

Define a notice taxonomy across apps, including any custom levels, so reusable apps and the design system agree on severity and styling.

## The level constants Every Django message has an integer **level**. `django.contrib.messages.constants` defines five: | Constant | Value | Default tag | Shortcut | |---|---|---|---| | `DEBUG` | 10 | `debug` | `messages.debug()` | | `INFO` | 20 | `info` | `messages.info()` | | `SUCCESS` | 25 | `success` | `messages.success()` | | `WARNING` | 30 | `warning` | `messages.warning()` | | `ERROR` | 40 | `error` | `messages.error()` | The shortcuts call `messages.add_message(request, level, message, extra_tags="", fail_silently=False)`. ## The recording threshold: MESSAGE_LEVEL The storage has a **minimum recorded level**. `BaseStorage.add()` silently ignores any message whose level is below it, and also ignores empty messages. The threshold comes from the `MESSAGE_LEVEL` setting; if it is not set, Django uses `INFO` (20). That is the answer to "why does `messages.debug()` do nothing?": `DEBUG` is 10, below the default threshold of 20, so the message is never queued. No error is raised. Ways to change it: - **Project-wide**: `MESSAGE_LEVEL = message_constants.DEBUG` in settings. The docs advise importing `django.contrib.messages.constants` directly in settings (not `django.contrib.messages`) to avoid circular imports, or using the numeric value. - **Per request**: `messages.set_level(request, messages.DEBUG)`; `messages.get_level(request)` reads it. `set_level(request, None)` restores the default. ## Tags: level tags, MESSAGE_TAGS and extra_tags A message's `tags` property is a space-separated string built from: 1. its **`extra_tags`**, a free string passed to `add_message()` or a shortcut, and 2. its **level tag**, looked up from the level. The default level tags are the lower-case names above. The `MESSAGE_TAGS` setting is **merged over** those defaults, so you override only what you need. A common case is a CSS framework that expects `danger` rather than `error`: ```python # settings.py from django.contrib.messages import constants as message_constants MESSAGE_TAGS = {message_constants.ERROR: "danger"} ``` Now `messages.error(request, "Upload failed.", extra_tags="avatar")` renders with `message.tags == "avatar danger"`, and `message.level_tag == "danger"`. ## Custom levels Levels are just integers, so you can define your own: ```python CRITICAL = 50 messages.add_message(request, CRITICAL, "Your account is locked.") ``` Give it a tag with `MESSAGE_TAGS = {50: "critical"}`; without an entry, its `level_tag` is an empty string. Keep custom values distinct from the built-ins, and remember that the threshold applies to them too. ## Using levels in templates The `messages` context processor also provides `DEFAULT_MESSAGE_LEVELS`, a mapping of level names to numbers: ```django {% for message in messages %} <li class="{{ message.tags }}"> {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}Important: {% endif %}{{ message }} </li> {% endfor %} ``` ## What add_message() does, step by step 1. It finds the storage that `MessageMiddleware` attached to the request; without it, `MessageFailure` (or nothing, with `fail_silently=True`). 2. The storage ignores the message if the text is empty. 3. It converts the level to `int` and compares it with the storage's level; anything lower is ignored. 4. It creates a `Message(level, message, extra_tags)` and queues it; `MessageMiddleware` stores queued messages when the response goes out. Because the check happens in step 3, the threshold in force **when the message is added** is the one that matters. Reusable apps that emit informational notices therefore cannot assume they will be shown: a project may raise `MESSAGE_LEVEL` to `WARNING` to keep the interface quiet, and every `info()` and `success()` call from those apps is then dropped without a trace. ## Common mistakes - Expecting `messages.debug()` to show in development without lowering `MESSAGE_LEVEL`. - Replacing the whole tag mapping by hand when `MESSAGE_TAGS` only needs the changed entries. - Putting CSS classes into the message text instead of `extra_tags`. - Setting `MESSAGE_LEVEL` in settings via `from django.contrib import messages`, which can cause a circular import. - Assuming filtering happens at display time; it happens when the message is **added**, so a lowered level later in the request does not bring back dropped messages.

  • How do you enable debug messages for a single Django request without changing MESSAGE_LEVEL?
    Call `messages.set_level(request, messages.DEBUG)` before adding them; it changes the threshold on that request's storage only and returns `True` if the storage exists. `messages.set_level(request, None)` restores the default. Messages added before the change were already filtered and do not come back.
  • What does message.level_tag return for a custom level such as 50 in Django if MESSAGE_TAGS has no entry for it?
    An empty string. `level_tag` looks the level up in the default tags merged with `MESSAGE_TAGS`, and 50 is in neither, so `message.tags` contains only any `extra_tags`. Add `MESSAGE_TAGS = {50: "critical"}` to give it a CSS-ready tag.

saying these in an interview costs you the question

  • messages.debug() is shown in development automatically when DEBUG = True
  • MESSAGE_TAGS replaces all default tags, so every level must be listed
  • Messages below the level are stored but hidden in templates
  • The default MESSAGE_LEVEL is DEBUG
  • extra_tags replaces the level tag in message.tags