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
- Use one of the built-in level strings: 'debug', 'info', 'success', 'warning', or 'error' (case is uppercased internally).
- For a custom level, register it first with messages.add_level(...) and pass the resulting integer (not the name string) to message_user.
- 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
- Pass integer message levels (messages.SUCCESS etc.) instead of strings to message_user.
- If a level comes from config/external input, validate it against messages.constants before use.
- Register custom levels once at import via messages.add_level and use the returned integer.
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
- The boolean and empty_value arguments to the @display decora
- Incorrect timezone setting: %s
- At least one model must be passed to register.
- site must subclass AdminSite
- Wrapped class must subclass ModelAdmin.
AI-assisted analysis of django/django@ae25a40be0 (2026-08-06).
Data as JSON: /api/errors/a9403e686e63010f.
Report an issue: GitHub.