Debug mode
app.config["DEBUG"] defaults to False, so this is a verify page rather than a
change-this page. What makes it worth its own entry is that one flag changes four
separate behaviours, and people who disable it for one reason often do not know about
the others.
Enabled by debug |
What it does |
|---|---|
| Interactive debugger | Traceback pages, plus a Python console — see the threat page |
| Reloader | Watches source files and restarts the process on change |
TESTING-adjacent behaviour |
Propagates exceptions instead of returning 500 |
| Template auto-reload | Re-reads templates on every request |
The console is the severe one and has its own page. This page is the switch.
DEBUGEnables the Werkzeug debugger and reloader. The default is correct, so this page is about what turns it on — and that is usually the environment rather than this key.
- accepts
True|False- default
False- set in
app.config
FLASK_DEBUGenvThe environment route into DEBUG, and the one that actually catches people — flask run --debug sets it to "1" for you, so a container whose command still carries that flag ships the debugger without any config file saying so.
- accepts
1|0- set in
process environment
TESTINGPropagates exceptions rather than converting them to 500s. Not the debugger, but worth checking alongside it — a test config leaking into production changes error behaviour.
- accepts
True|False- default
False- set in
app.config
import os
# explicit comparison, never a bare getenvapp.config["DEBUG"] = os.getenv("FLASK_DEBUG") == "1"The comparison is the point. In Python every non-empty string is truthy, so:
os.getenv("FLASK_DEBUG") # "0" -> truthy -> debug ONos.getenv("FLASK_DEBUG") # "false" -> truthy -> debug ONos.getenv("FLASK_DEBUG") == "1" # "0" -> False -> debug offA variable that reads as switched off in a compose file and is switched on in the process is the single most common route into the debugger console.
python3 -c "from app import app; print('DEBUG =', app.config['DEBUG'], '| TESTING =', app.config['TESTING'])"Both should be False. Check the environment separately, because Flask reads
FLASK_DEBUG when the CLI starts the application:
env | grep -iE '^FLASK_(DEBUG|ENV|APP)='And confirm from outside that a failure returns nothing interesting:
curl -s https://app.example.com/__no_such_path__/ | grep -ciE 'traceback|werkzeug|console|File "'Zero is correct.
FLASK_ENV is gone
Section titled “FLASK_ENV is gone”Flask 2.3 removed FLASK_ENV, which used to set debug mode as a side effect of being
development. A deployment still setting it gets no error and no debugger — the
variable is simply ignored.
That cuts both ways. It means an old FLASK_ENV=development in a compose file is inert
rather than dangerous, and it means a team that believed they were controlling debug
mode through it has been controlling nothing. FLASK_DEBUG is the variable that
matters now, and the check above reads the setting rather than the variable for exactly
this reason.
The reloader is a separate hazard
Section titled “The reloader is a separate hazard”debug=True enables the reloader, which runs the application in a child process and
watches files. Two consequences worth knowing.
It doubles the process count, so anything counting workers or holding a lock at import time behaves differently under debug than it does in production — which is a category of “works on my machine” that is hard to diagnose.
And it means the application re-executes your module on every save. Anything with a side effect at import — a migration, a scheduled job registration, a request to a third party — happens again each time.
Neither is a security issue on its own. Both are reasons the development invocation should not be what runs in production, which is the production server page.
Turning debug off removes the traceback page your team may be relying on, so the replacement has to be in place first — see the Breaks note on the threat page for the error handler and logging setup.
Turning off the reloader changes the development loop, not production. If your team
runs flask run locally, keep debug on there — this control is about what ships, and a
development machine is not what ships.
The one genuine surprise is exception propagation: with debug off, an unhandled
exception becomes a 500 response instead of crashing the request loudly. Tests that
relied on the exception surfacing will start seeing a 500 instead. Set
PROPAGATE_EXCEPTIONS = True in your test configuration rather than turning debug back
on to get that behaviour.
Related
Section titled “Related”- The Werkzeug debugger console — what debug mode actually exposes
- The production server — why app.run() is the wrong invocation regardless