Flask Skill
Production-tested patterns for Flask with the application factory pattern, Blueprints, and Flask-SQLAlchemy.
Latest Versions (verified January 2026):
- Flask: 3.1.2
- Flask-SQLAlchemy: 3.1.1
- Flask-Login: 0.6.3
- Flask-WTF: 1.2.2
- Werkzeug: 3.1.5
- Python: 3.9+ required (3.8 dropped in Flask 3.1.0)
Quick Start
Project Setup with uv
uv init my-flask-app
cd my-flask-app
uv add flask flask-sqlalchemy flask-login flask-wtf python-dotenv
uv run flask --app app run --debug
Minimal Working Example
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return {"message": "Hello, World!"}
if __name__ == "__main__":
app.run(debug=True)
Run: uv run flask --app app run --debug
Known Issues Prevention
This skill prevents 9 documented issues:
Issue #1: stream_with_context Teardown Regression (Flask 3.1.2)
Error: KeyError in teardown functions when using stream_with_context
Source: GitHub Issue #5804
Why It Happens: Flask 3.1.2 introduced a regression where stream_with_context triggers teardown_request() calls multiple times before response generation completes. If teardown callbacks use g.pop(key) without a default, they fail on the second call.
Prevention:
@app.teardown_request
def _teardown_request(_):
g.pop("hello")
@app.teardown_request
def _teardown_request(_):
g.pop("hello", None)
Status: Will be fixed in Flask 3.2.0 as side effect of PR #5812. Until then, ensure all teardown callbacks are idempotent.
Issue #2: Async Views with Gevent Incompatibility
Error: RuntimeError when handling concurrent async requests with gevent
Source: GitHub Issue #5881
Why It Happens: Asgiref fails when gevent monkey-patching is active. Asyncio expects a single event loop per OS thread, but gevent's monkey-patching makes threading.Thread create greenlets instead of real threads, causing both loops to run on the same physical thread and block each other.
Prevention: Choose either async (with asyncio/uvloop) OR gevent, not both. If you must use both:
import asyncio
import gevent.monkey
import gevent.selectors
from flask import Flask
gevent.monkey.patch_all()
loop = asyncio.EventLoop(gevent.selectors.DefaultSelector())
gevent.spawn(loop.run_forever)
class GeventFlask(Flask):
def async_to_sync(self, func):
def run(*args, **kwargs):
coro = func(*args, **kwargs)
future = asyncio.run_coroutine_threadsafe(coro, loop)
return future.result()
return run
app = GeventFlask(__name__)
Note: This "defeats the whole purpose of both" (maintainer comment). Individual async requests work, but concurrent requests fail without this workaround.
Issue #3: Test Client Session Not Updated on Redirect
Error: Session state incorrect after follow_redirects=True in tests
Source: GitHub Issue #5786
Why It Happens: In Flask < 3.1.2, the test client's session wasn't correctly updated after following redirects.
Prevention:
def test_login_redirect(client):
response = client.post('/login',
data={'email': '[email protected]', 'password': 'pass'},
follow_redirects=True)
assert 'user_id' in session
response = client.post('/login', data={...})
assert response.status_code == 302
response = client.get(response.location)
Status: Fixed in Flask 3.1.2. Upgrade to latest version.
Issue #4: Application Context Lost in Threads (Community-sourced)
Error: RuntimeError: Working outside of application context in background threads
Source: Sentry.io Guide
Why It Happens: When passing current_app to a new thread, you must unwrap the proxy object using _get_current_object() and push app context in the thread.
Prevention:
from flask import current_app
import threading
def background_task():
app_name = current_app.name
@app.route('/start')
def start_task():
thread = threading.Thread(target=background_task)
thread.start()
def background_task(app):
with app.app_context():
app_name = app.name
@app.route('/start')
def start_task():
app = current_app._get_current_object()
thread = threading.Thread(target=background_task, args=(app,))
thread.start()
Verified: Common pattern in production applications, documented in official Flask docs.
Issue #5: Flask-Login Session Protection Unexpected Logouts (Community-sourced)
Error: Users logged out unexpectedly when IP address changes
Source: Flask-Login Docs
Why It Happens: Flask-Login's "strong" session protection mode deletes the entire session if session identifiers (like IP address) change. This affects users on mobile networks or VPNs.
Prevention:
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = "basic"
Note: By default, Flask-Login allows concurrent sessions (same user on multiple browsers). To prevent this, implement custom session tracking.
Verified: Official Flask-Login documentation, multiple 2024 blog posts.
Issue #6: CSRF Protection Cache Interference (Community-sourced)
Error: Form submissions fail with "CSRF token missing/invalid" on cached pages
Source: Flask-WTF Docs
Why It Happens: If webserver cache policy caches pages longer than WTF_CSRF_TIME_LIMIT, browsers serve cached pages with expired CSRF tokens.
Prevention:
WTF_CSRF_TIME_LIMIT = None
@app.after_request
def add_cache_headers(response):
if request.method == 'GET' and 'form' in request.endpoint:
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
return response
Verified: Official Flask-WTF documentation warning, security best practices guides from 2024.
Issue #7: Per-Request max_content_length Override (New Feature)
Feature: Flask 3.1.0 added ability to customize Request.max_content_length per-request
Source: Flask 3.1.0 Release Notes
Usage:
from flask import Flask, request
app = Flask(__name__)
app.config['MAX_CONTENT_LENG