django/django · error · ValueError

Bad message level string: `%s`. Possible values are: %s

Error message

Bad message level string: `%s`. Possible values are: %s

What it means

A ValueError raised in ModelAdmin.message_user when the level argument is a string that does not correspond to a constant in django.contrib.messages.constants (e.g. not one of DEBUG/INFO/SUCCESS/WARNING/ERROR uppercased). message_user tolerates a small set of level strings by getattr-ing the constants module; anything else raises with a list of valid possibilities.

Source

Thrown at django/contrib/admin/options.py:1435

        self, request, message, level=messages.INFO, extra_tags="", fail_silently=False
    ):
        """
        Send a message to the user. The default implementation
        posts a message using the django.contrib.messages backend.

        Exposes almost the same API as messages.add_message(), but accepts the
        positional arguments in a different order to maintain backwards
        compatibility. For convenience, it accepts the `level` argument as
        a string rather than the usual level number.
        """
        if not isinstance(level, int):
            # attempt to get the level if passed a string
            try:
                level = getattr(messages.constants, level.upper())
            except AttributeError:
                levels = messages.constants.DEFAULT_TAGS.values()
                levels_repr = ", ".join("`%s`" % level for level in levels)
                raise ValueError(
                    "Bad message level string: `%s`. Possible values are: %s"
                    % (level, levels_repr)
                )

        messages.add_message(
            request, level, message, extra_tags=extra_tags, fail_silently=fail_silently
        )

    def save_form(self, request, form, change):
        """
        Given a ModelForm return an unsaved instance. ``change`` is True if
        the object is being changed, and False if it's being added.
        """
        return form.save(commit=False)

    def save_model(self, request, obj, form, change):
        """
        Given a model instance save it to the database.

View on GitHub (pinned to ae25a40be0)

Solutions

  1. Use one of the built-in level strings: 'debug', 'info', 'success', 'warning', or 'error' (case is uppercased internally).
  2. For a custom level, register it first with messages.add_level(...) and pass the resulting integer (not the name string) to message_user.
  3. If the level comes from external input, validate it against the set of DEFAULT_TAGS before calling message_user.

Example fix

// before
self.message_user(request, 'Saved', level='notice')
// after
from django.contrib import messages
self.message_user(request, 'Saved', level=messages.SUCCESS)
# or a custom registered level:
# messages.add_level('NOTICE', 25); self.message_user(request, 'Saved', level=25)
Defensive patterns

Strategy: validation

Validate before calling

from django.contrib.messages import constants as msg_constants

def normalize_level(level):
    if isinstance(level, int):
        return level
    upper = level.upper() if isinstance(level, str) else level
    if not hasattr(msg_constants, upper):
        raise ValueError(
            f'Bad message level string: `{level}`. '
            f'Use one of: debug, info, success, warning, error.'
        )
    return getattr(msg_constants, upper)

Type guard

from django.contrib.messages import constants as msg_constants

def is_valid_level_string(level) -> bool:
    return isinstance(level, str) and hasattr(msg_constants, level.upper())

Try / catch

try:
    self.message_user(request, msg, level=raw_level)
except ValueError as e:
    if 'Bad message level string' in str(e):
        self.message_user(request, msg, level='info')  # safe fallback
    else:
        raise

Prevention

When it happens

Trigger: Calling self.message_user(request, 'hi', level='notice') where 'notice' is not a built-in level. Passing a custom string level without first registering it via messages.add_level. Passing a typo like 'sucess'.

Common situations: Trying to use a custom message level name that was never registered. Copying code that assumed an extended level set. Sending a level value loaded from a config file as a raw string.

Related errors


AI-assisted analysis of django/django@ae25a40be0 (2026-08-06). Data as JSON: /api/errors/a9403e686e63010f. Report an issue: GitHub.