| name | debug:flask |
| description | Debug Flask applications systematically with this comprehensive troubleshooting skill. Covers routing errors (404/405), Jinja2 template issues, application context problems, SQLAlchemy session management, blueprint registration failures, and circular import resolution. Provides structured four-phase debugging methodology with Flask-specific tools including Werkzeug debugger, Flask-DebugToolbar, and Flask shell for interactive investigation. |
Flask Debugging Guide
A systematic approach to debugging Flask applications using established patterns and Flask-specific tooling.
Common Error Patterns
HTTP Status Code Errors
404 Not Found - Routing Issues
- Route decorator not matching requested URL
- Typos in route definitions or endpoint names
- Blueprint not registered with application
- Missing trailing slash mismatch (
strict_slashes setting)
- URL rules registered after first request
500 Internal Server Error
- Unhandled exceptions in view functions
- Template rendering errors
- Database connection failures
- Malformed JSON request data (missing
Content-Type: application/json)
- Circular imports preventing app initialization
405 Method Not Allowed
- HTTP method not specified in
@app.route() methods parameter
- Form submission using wrong method (GET vs POST)
Jinja2 Template Errors
TemplateNotFound
- Template not in
templates/ directory
- Custom template folder not configured
- Blueprint template folder misconfigured
UndefinedError
- Variable not passed to
render_template()
- Typo in template variable name
- Accessing attribute on None object
TemplateSyntaxError
- Unclosed blocks (
{% if %} without {% endif %})
- Invalid filter usage
- Incorrect macro definitions
Application Context Errors
RuntimeError: Working outside of application context
- Accessing
current_app outside request/CLI context
- Database operations outside
with app.app_context():
- Celery tasks not properly configured with app context
RuntimeError: Working outside of request context
- Accessing
request, session, or g outside view function
- Background thread without request context
SQLAlchemy Session Issues
DetachedInstanceError
- Accessing lazy-loaded relationship outside session
- Object expired after transaction commit
- Session closed prematurely
InvalidRequestError: Object already attached to session
- Adding object already in different session
- Improper session management in tests
OperationalError: Connection pool exhausted
- Connections not being returned to pool
- Missing
db.session.close() or db.session.remove()
- Long-running transactions
Blueprint Registration Problems
Blueprint registration failures
- Circular imports between blueprints
- Duplicate blueprint names
- Blueprint registered after first request
- URL prefix conflicts
Import and Module Errors
ImportError: cannot import name 'Flask'
- Circular dependency in modules
- File named
flask.py shadowing the package
- Virtual environment not activated
ModuleNotFoundError: No module named 'flask'
- Virtual environment not activated
- Flask not installed in current environment
- Wrong Python interpreter
Debugging Tools
Built-in Flask Debugger (Werkzeug)
app.run(debug=True)
The Werkzeug debugger provides:
- Interactive traceback with code context
- In-browser Python REPL at each stack frame
- Variable inspection at each level
- PIN-protected access (check terminal for PIN)
Security Warning: Never enable debug mode in production. The debugger allows arbitrary code execution from the browser.
Python Debugger (pdb/breakpoint)
def my_view():
data = get_data()
breakpoint()
return process(data)
Flask Shell
flask shell
>>> from app.models import User
>>> User.query.all()
>>> app.config['DEBUG']
>>> app.url_map
Logging Module
import logging
from flask import Flask
app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)
app.logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.WARNING)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
app.logger.addHandler(file_handler)
@app.route('/api/data')
def get_data():
app.logger.debug('Processing request for /api/data')
try:
result = fetch_data()
app.logger.info(f'Successfully fetched {len(result)} records')
return jsonify(result)
except Exception as e:
app.logger.error(f'Error fetching data: {e}', exc_info=True)
raise
Flask-DebugToolbar
from flask import Flask
from flask_debugtoolbar import DebugToolbarExtension
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev-secret-key'
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
toolbar = DebugToolbarExtension(app)
Provides sidebar panels for:
- Request/response headers
- Template rendering details
- SQLAlchemy queries (with query time)
- Route matching information
- Configuration values
- Logging messages
SQLAlchemy Query Logging
import logging
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
app.config['SQLALCHEMY_ECHO'] = True
app.config['DEBUG_TB_PANELS'] = [
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
]
The Four Phases of Flask Debugging
Phase 1: Reproduce and Isolate
Capture the exact error state:
flask run --debug
flask routes
flask shell
>>> app.config
Minimal reproduction:
def test_failing_route(client):
response = client.get('/api/broken-endpoint')
print(f"Status: {response.status_code}")
print(f"Data: {response.get_data(as_text=True)}")
assert response.status_code == 200
Questions to answer:
- Does error occur on every request or intermittently?
- What is the exact HTTP status code?
- What does the traceback show?
- What was the request payload?
- What environment variables are set?
Phase 2: Gather Information
Collect comprehensive context:
@app.before_request
def log_request_info():
app.logger.debug('Headers: %s', dict(request.headers))
app.logger.debug('Body: %s', request.get_data())
app.logger.debug('Args: %s', dict(request.args))
@app.after_request
def log_response_info(response):
app.logger.debug('Response Status: %s', response.status)
return response
Check configuration:
flask shell
>>> from flask import current_app
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
>>> current_app.url_map
>>> current_app.extensions
Database state:
>>> from app import db
>>> db.session.execute(text("SELECT 1")).fetchone()
>>> User.query.count()
>>> db.engine.pool.status()
Phase 3: Hypothesize and Test
Form hypotheses based on error patterns:
| Error Pattern | Likely Cause | Test |
|---|
| 404 on valid route | Blueprint not registered | Check app.url_map |
| 500 with no traceback | Error before request context | Check create_app() |
| DetachedInstanceError | Lazy load outside session | Add lazy='joined' |
| Connection refused | DB not running | Test connection string |
Test hypotheses systematically:
flask routes | grep expected_endpoint
flask shell
>>> from app import db
>>> db.session.execute(text("SELECT 1"))
flask shell
>>> import os
>>> os.environ.get('DATABASE_URL')
>>> app.config.get('DATABASE_URL')
Phase 4: Fix and Verify
Apply fix with minimal changes:
def test_user_creation(client):
response = client.post('/api/users', json={'name': 'Test'})
assert response.status_code == 201
assert 'id' in response.get_json()
pytest tests/test_users.py::test_user_creation -v
Verify fix doesn't introduce regressions:
pytest
pytest --cov=app --cov-report=term-missing
Document the fix:
@app.route('/api/users', methods=['POST'])
def create_user():
if not request.is_json:
return jsonify(error='Content-Type must be application/json'), 400
...
Quick Reference Commands
flask routes
flask shell
flask db status
flask db current
flask db history
flask run --debug
flask run --host=0.0.0.0 --port=5000
flask shell -c "print(app.config)"
flask shell -c "from app import db; print(db.session.execute(text('SELECT 1')).fetchone())"
flask shell -c "import os; print(os.environ.get('FLASK_ENV'))"
flask shell -c "print(list(app.extensions.keys()))"
Flask-Specific Debugging Patterns
Debug Application Factory Issues
def create_app(config_name='default'):
app = Flask(__name__)
print(f"Loading config: {config_name}")
app.config.from_object(config[config_name])
db.init_app(app)
print(f"DB initialized: {db}")
from app.api import api_bp
app.register_blueprint(api_bp, url_prefix='/api')
print(f"Registered routes: {[r.rule for r in app.url_map.iter_rules()]}")
return app
Debug Request/Response Cycle
from flask import g, request
import time
@app.before_request
def before_request():
g.start_time = time.time()
g.request_id = request.headers.get('X-Request-ID', 'unknown')
app.logger.info(f"[{g.request_id}] {request.method} {request.path}")
@app.after_request
def after_request(response):
duration = time.time() - g.start_time
app.logger.info(
f"[{g.request_id}] {response.status_code} ({duration:.3f}s)"
)
return response
@app.teardown_request
def teardown_request(exception):
if exception:
app.logger.error(f"[{g.request_id}] Exception: {exception}")
Debug SQLAlchemy N+1 Queries
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import event
db = SQLAlchemy()
if app.debug:
@event.listens_for(db.engine, "before_cursor_execute")
def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
app.logger.debug(f"SQL: {statement}")
app.logger.debug(f"Params: {parameters}")
Debug Template Rendering
@app.context_processor
def debug_context():
if app.debug:
return {
'debug_info': {
'endpoint': request.endpoint,
'view_args': request.view_args,
'url': request.url,
}
}
return {}
{% if config.DEBUG %}
<pre>
Endpoint: {{ debug_info.endpoint }}
View Args: {{ debug_info.view_args }}
URL: {{ debug_info.url }}
</pre>
{% endif %}
Debug Blueprint Registration
def register_blueprints(app):
"""Register all blueprints with debugging."""
from app.api import api_bp
from app.auth import auth_bp
blueprints = [
(api_bp, '/api'),
(auth_bp, '/auth'),
]
for bp, prefix in blueprints:
app.logger.debug(f"Registering blueprint: {bp.name} at {prefix}")
try:
app.register_blueprint(bp, url_prefix=prefix)
app.logger.debug(f"Successfully registered {bp.name}")
except Exception as e:
app.logger.error(f"Failed to register {bp.name}: {e}")
raise
Error Handlers for Better Debugging
from flask import jsonify
from werkzeug.exceptions import HTTPException
@app.errorhandler(Exception)
def handle_exception(e):
"""Handle all unhandled exceptions."""
app.logger.exception(f"Unhandled exception: {e}")
if isinstance(e, HTTPException):
return jsonify(error=e.description), e.code
if app.debug:
return jsonify(
error=str(e),
type=type(e).__name__,
traceback=traceback.format_exc()
), 500
return jsonify(error="Internal server error"), 500
@app.errorhandler(404)
def not_found(e):
app.logger.warning(f"404: {request.url}")
return jsonify(error="Resource not found", path=request.path), 404
@app.errorhandler(500)
def internal_error(e):
db.session.rollback()
app.logger.error(f"500 error: {e}")
return jsonify(error="Internal server error"), 500
Production Debugging with Sentry
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
sentry_sdk.init(
dsn="your-sentry-dsn",
integrations=[
FlaskIntegration(),
SqlalchemyIntegration(),
],
traces_sample_rate=0.1,
environment=os.environ.get('FLASK_ENV', 'production'),
)
Environment-Specific Configuration
class Config:
"""Base configuration."""
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key-change-in-prod')
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
"""Development configuration with debugging enabled."""
DEBUG = True
SQLALCHEMY_ECHO = True
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""Production configuration - no debug features."""
DEBUG = False
SQLALCHEMY_ECHO = False
SECRET_KEY = os.environ['SECRET_KEY']
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig,
}
Debugging Checklist
Before escalating or spending extensive time:
References