skip to content

In Django, what does a visitor see when a view raises an unhandled exception with DEBUG = True, and what changes with DEBUG = False?

level: juniorimportance: must knowfreq 72%

answer

  1. one setting, two audiences
  2. technical 500 page
  3. traceback, locals, request, settings
  4. 500.html plus a mail to ADMINS

basics

~20 s

With DEBUG = True Django returns its technical 500 page: traceback, local variables, request data and most settings. With DEBUG = False the visitor gets the 500.html template, and the details go to logging and ADMINS emails.

solid answer

~40 s

`DEBUG` decides who sees the crash. With `DEBUG = True`, Django catches the exception and returns the **technical 500 page**: exception type and value, the full traceback with source lines and each frame's local variables, the request's GET, POST, FILES, COOKIES and META, and the settings, with only name-matched values such as `SECRET_KEY` starred. An `Http404` gets the technical 404 page listing the URL patterns tried. With `DEBUG = False`, Django calls the 500 handler, which renders `500.html` (or a bare 'Server Error (500)' page), logs the error on `django.request`, and the default logging emails the people in `ADMINS`. The global default is `False`; `startproject` writes `True` into the new `settings.py`.

code

python · 8 lines
python
# settings.py
import os

DEBUG = os.environ.get("DJANGO_DEBUG") == "1"  # the global default is False
ALLOWED_HOSTS = ["shop.example.com"]

ADMINS = ["[email protected]"]  # who receives 500 reports when DEBUG is False
SERVER_EMAIL = "[email protected]"  # sender of those reports

go deeper

for a junior

Know that DEBUG = True shows a traceback page with settings and variables, and that production must run with DEBUG = False, where users get 500.html.

for a middle

Explain what each section of the technical page exposes, why 404.html is ignored in debug mode, and that the details go to logging and ADMINS emails once DEBUG is off.

for a senior

Point out that the page stars settings by name only and ignores the sensitive-data decorators in debug mode, so a staging box with DEBUG on and real data leaks.

for a principal

Frame debug output as an environment policy: DEBUG driven by deployment config, no shared environments with real data in debug mode, and error detail routed to people rather than pages.

## The switch: `DEBUG` Django has one boolean setting, `DEBUG`, that changes how an unhandled exception is presented. Its global default in `django/conf/global_settings.py` is `False`, but `django-admin startproject` writes `DEBUG = True` into the generated `settings.py` for convenience, which is why every new project starts in debug mode. When a view raises an exception that nothing catches, Django's request handler turns it into a response; which response depends on `DEBUG`. ## What the technical 500 page shows With `DEBUG = True`, the handler calls the **technical 500 response** (`django.views.debug.technical_500_response`). It is built for the developer, not the visitor: | Section | What it contains | |---|---| | Summary | exception type, exception value, request method and URL, the view that raised, Django and Python versions | | Traceback | every frame with surrounding source lines and a **Local vars** table per frame | | Request information | the user, `GET`, `POST`, `FILES`, `COOKIES` and `META` (headers and server variables) | | Settings | every upper-case setting, with values whose **names** match `API`, `AUTH`, `TOKEN`, `KEY`, `SECRET`, `PASS`, `SIGNATURE` replaced by stars | The page ends with the note that you are seeing it because `DEBUG = True`. A client that prefers `text/plain` gets the same report as plain text. The same page (with status 400) is used for `SuspiciousOperation` and `BadRequest` in debug mode. Two things matter for security: - the starring of settings works **by name**, so a custom setting such as `DATABASE_URL` holding a password is printed in clear; - the `sensitive_variables` and `sensitive_post_parameters` annotations are **ignored** while `DEBUG` is `True`, because the default `SafeExceptionReporterFilter.is_active()` returns `settings.DEBUG is False`. Local variables and POST fields appear exactly as they are. ## The technical 404 page When `Http404` is raised (by `get_object_or_404`, or because no URL pattern matched) and `DEBUG = True`, Django shows the **technical 404 page** instead of `404.html`. It lists the URL patterns Django tried, in order, and the message passed to `Http404`. A fresh project with no patterns of its own shows the 'install worked successfully' welcome page instead. Your `404.html` is never used while `DEBUG` is on, which surprises people testing their error templates locally. ## What production users see With `DEBUG = False`, the handler resolves the project's 500 handler. The default, `django.views.defaults.server_error`, does the following: 1. It looks for a template called `500.html` on the configured template loaders. 2. If it finds one, it renders it with an **empty context** and returns it with status 500. 3. If there is none, it returns a minimal HTML page whose heading is 'Server Error (500)'. 404s follow the same pattern with `404.html` and a built-in 'Not Found' page. The visitor learns nothing about your code. ## Where the details go instead The traceback is not lost. Django logs the 5xx response on the `django.request` logger, and Django's default logging configuration attaches an email handler that, when `DEBUG` is `False` and `ADMINS` is not empty, mails the same report (as text, with no page chrome) to the addresses in `ADMINS`. Those emails do honour the sensitive-data decorators, because the filter is active once `DEBUG` is off. A separate setting, `DEBUG_PROPAGATE_EXCEPTIONS` (default `False`), makes the handler re-raise instead of building any 500 response; it exists for test and server setups that want the raw exception. ## Why the debug page must never reach production - It prints file paths, installed apps, middleware and every setting whose name does not match the pattern. - It prints local variables and POST bodies unfiltered: passwords being checked, card numbers, tokens read from a database row. - It is not restricted to `INTERNAL_IPS`; any visitor who triggers an error sees it. That restriction belongs to tools like the debug toolbar, not to the error page. - Debug mode also makes Django keep a record of every SQL query it runs, which grows memory on a long-lived process. The rule in Django's own settings reference is blunt: never deploy with `DEBUG` turned on. Drive it from the environment so the production value cannot be forgotten.

  • In Django, what happens on an Http404 while DEBUG is True, even if the project has a 404.html?
    Django shows the technical 404 page and ignores `404.html`. The page lists every URL pattern tried, in order, plus the `Http404` message, which is how you debug a routing miss. A project whose URLconf has no patterns of its own shows the welcome page instead. To see your real `404.html`, run with `DEBUG = False` (and a valid `ALLOWED_HOSTS`).
  • Does Django's technical 500 page hide secrets at all?
    Partly. Settings, `META` entries and cookies whose names match `API`, `AUTH`, `TOKEN`, `KEY`, `SECRET`, `PASS`, `SIGNATURE` or `HTTP_COOKIE` are starred, and so is the session cookie. Local variables and POST fields are not filtered in debug mode, because the filter's `is_active()` is true only when `DEBUG` is `False`. A setting named `DATABASE_URL` is shown in clear.

saying these in an interview costs you the question

  • DEBUG = True is acceptable in production if nobody knows the URLs
  • The debug page hides every password and API key automatically
  • With DEBUG = False the traceback of a 500 is simply discarded
  • 404.html is rendered for 404s even while DEBUG is True
  • The technical 500 page is only shown to requests from INTERNAL_IPS