mir-backend-python-flask
Make It Right (Flask module). Flask 3 specific reliability augmentation. Use alongside the mir-backend skill when the target stack is Flask — it carries the mechanical footguns that the framework-agnostic skill deliberately omits: app/request context misuse (current_app/request/g outside context), missing input validation and object-level authorization, SQLAlchemy session scoping and teardown, app-factory pattern and circular import avoidance, offloading heavy work to Celery/RQ, config/secret safety (debug=True RCE, SECRET_KEY), and Alembic migration safety via Flask-Migrate. TRIGGER only when the Python backend stack is Flask — building, reviewing, or debugging a Flask route, blueprint, extension, SQLAlchemy session, or migration. Always loads TOGETHER WITH mir-backend (the gates) and mir-backend-python (CPython runtime concerns: GIL, async/sync, fork-safety, cold start); this module only adds Flask library mechanics. SKIP for Django, FastAPI, or any non-Flask stack (those get their own mir-backend-python-<framework> module), and for non-Python runtimes.
Works with
---
name: mir-backend-python-flask
description: Make It Right (Flask module). Flask 3 specific reliability augmentation. Use alongside the mir-backend skill when the target stack is Flask — it carries the mechanical footguns that the framework-agnostic skill deliberately omits: app/request context misuse (current_app/request/g outside context), missing input validation and object-level authorization, SQLAlchemy session scoping and teardown, app-factory pattern and circular import avoidance, offloading heavy work to Celery/RQ, config/secret safety (debug=True RCE, SECRET_KEY), and Alembic migration safety via Flask-Migrate. TRIGGER only when the Python backend stack is Flask — building, reviewing, or debugging a Flask route, blueprint, extension, SQLAlchemy session, or migration. Always loads TOGETHER WITH mir-backend (the gates) and mir-backend-python (CPython runtime concerns: GIL, async/sync, fork-safety, cold start); this module only adds Flask library mechanics. SKIP for Django, FastAPI, or any non-Flask stack (those get their own mir-backend-python-<framework> module), and for non-Python runtimes.
license: Apache-2.0
---
# /mir-backend-python-flask · Make It Right (Flask)
Bottom tier of the chain: `mir-backend` (generic gates) → `mir-backend-python` (CPython runtime model) → **this** (Flask library mechanics). Run the gates first; load the Python runtime tier for the concurrency/process model; reach for *this* at Gate 5 (design mechanics), Gate 6 (implementation), and Gate 7 review. **Runtime-level concerns (GIL, async-vs-sync, blocking the event loop, fork-safe pools, cold start) live in `mir-backend-python` — not here.**
**Stack assumed:** Flask 3 · Flask-SQLAlchemy (SQLAlchemy 2.x) · PostgreSQL · Flask-Migrate / Alembic · Celery or RQ for background work. If the project uses a bare SQLAlchemy `scoped_session` without Flask-SQLAlchemy, apply the session-scoping rules directly — the pattern is the same, the scaffold differs.
## The Flask footguns AI walks into most
These are the stack-specific cousins of the failure-mode catalog. Each is something Flask code gets wrong even when the *logic* is right.
### 1. App and request context — the three magic proxies and where they die
`current_app`, `request`, and `g` are thread-local (or context-var-local) proxies that only resolve inside an active application or request context. Using them at import time, in a module-level expression, or in a background thread/task raises `RuntimeError: Working outside of application context` or `RuntimeError: Working outside of request context`. AI writes context-access code at class-definition time or in a `threading.Thread` target and ships a crash that never surfaces in a single-threaded dev run.
**`current_app`** — resolves to the app bound on the current context stack. Access it inside a view function, a CLI command, or after explicitly pushing a context.
**`g`** — request-scoped scratch space. It is torn down at the end of each request. Do not use `g` as an inter-request cache or a way to share state between requests — it is not. Each request gets a fresh `g`.
**Background threads:** any thread spawned from a request (e.g., `threading.Thread(target=fn).start()`) has no context by default. Push one explicitly:
```python
# WRONG — raises RuntimeError in the thread
def background():
result = some_db_query() # uses current_app implicitly
thread = threading.Thread(target=background)
thread.start()
# RIGHT
app = current_app._get_current_object() # get the real app, not the proxy
def background(app):
with app.app_context():
result = some_db_query()
thread = threading.Thread(target=background, args=(app,))
thread.start()
```
**Celery/RQ tasks:** same issue — tasks run in a worker process with no Flask context unless the task sets one up. The standard pattern is a `ContextTask` base class that pushes `app.app_context()` before calling the task body.
### 2. Unopinionated by design — validation and authorization are easy to forget
Flask has no built-in request validation, no enforced authentication scheme, and no object-level permission model. AI builds a route that parses `request.json` directly and applies business logic, forgetting both layers. The result is a service that accepts malformed input and happily returns another user's data.
**Input validation:** use Pydantic (v2 `model_validate`) or marshmallow to parse and validate request bodies before they touch the business layer. Never use raw `request.json.get("field")` for fields that have type or range constraints.
```python
from pydantic import BaseModel, ValidationError
class CreateOrderBody(BaseModel):
product_id: int
quantity: int
@app.post("/orders")
def create_order():
try:
body = CreateOrderBody.model_validate(request.get_json())
except ValidationError as e:
abort(422, description=e.errors())
...
```
**Object-level authorization (IDOR):** decorating a route with `@login_required` (flask-login) confirms the caller is authenticated; it does **not** confirm the caller owns the resource being loaded. Always assert ownership after loading the object:
```python
@app.get("/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = db.get_or_404(Order, order_id)
if order.user_id != current_user.id:
abort(403) # object-level check — not implied by @login_required
return order_schema.dump(order)
```
### 3. SQLAlchemy session scoping — leaked sessions corrupt state
A `Session` is a unit of work bound to a single connection. Sharing one session across requests or threads corrupts it — you get stale data, `DetachedInstanceError`, or silent cross-request data bleed.
**Flask-SQLAlchemy** handles scoping automatically: `db.session` is a `scoped_session` keyed on the current thread (or async context). It is removed in `teardown_appcontext` — you do not need to manage the lifecycle manually beyond `db.session.remove()` being called by the extension.
**Bare SQLAlchemy:** if you're not using Flask-SQLAlchemy, create a `scoped_session` and register teardown explicitly:
```python
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker(bind=engine))
@app.teardown_appcontext
def shutdown_session(exception):
Session.remove() # returns the session to the pool and clears the scope
```
**Commit/rollback boundary must be explicit.** Flask-SQLAlchemy does not autocommit. Call `db.session.commit()` when the work should persist; if an exception propagates, call `db.session.rollback()` (or rely on `teardown_appcontext` to roll back via `Session.remove()` — but only if you haven't already committed half the work). An unfinished transaction that is only `remove()`d without commit is rolled back, which is correct — but explicit rollback in an `except` block is clearer and safer.
```python
@app.post("/transfer")
def transfer():
try:
from_acct.balance -= amount
to_acct.balance += amount
db.session.commit()
except Exception:
db.session.rollback()
raise
```
### 4. Blueprint organization and the app factory — avoid import-time global state
A module-level `app = Flask(__name__)` and extensions bound directly to it at import time (`db = SQLAlchemy(app)`) create a global singleton that makes testing impossible (you can't swap config between tests) and causes circular imports as blueprints try to import from the module that imports them.
**App factory pattern:** `create_app(config=None)` instantiates the app, configures it, calls `extension.init_app(app)`, and registers blueprints — all inside the function. Extensions are created at module level but bound to the app lazily via `init_app`:
```python
# extensions.py — no app reference at module level
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# factory.py
from .extensions import db
def create_app(config=None):
app = Flask(__name__)
app.config.from_object(config or "config.ProductionConfig")
db.init_app(app)
from .orders import orders_bp
app.register_blueprint(orders_bp, url_prefix="/orders")
return app
```
**Circular imports:** blueprints that import `db` from `extensions.py` (not from the app module) break the cycle. Never import from the factory module inside a blueprint.
**Testing:** each test calls `create_app("config.TestingConfig")`, so isolation is trivial. A global `app` singleton makes this impossible without monkey-patching config.
### 5. Heavy and durable work — Flask workers block
Flask runs one request per thread (or coroutine in async mode). A route that does a 30-second video encode, a large data export, or a third-party API call with no timeout blocks that worker for the duration, reducing throughput to `workers - 1` for all other requests.
**Offload to Celery or RQ:**
- The route creates the job record, enqueues the task with an idempotency key, and returns `202 Accepted` with a job ID.
- The worker executes the task; its result is stored in the DB or Redis.
- The client polls or receives a webhook.
```python
@app.post("/exports")
def start_export():
job = ExportJob(user_id=current_user.id, status="queued")
db.session.add(job)
db.session.commit()
run_export.apply_async(args=[job.id], task_id=str(job.id))
return {"job_id": job.id}, 202
```
**Idempotency:** Celery tasks must be idempotent (re-delivery safe). Store the outcome keyed on the job ID; a retry that finds a completed record returns without re-executing. This is the at-least-once / idempotency rule from `mir-backend` landing in Flask's Celery integration.
**Flask's async support (Flask 2+):** `async def` route handlers are supported but run in a threadpool under the hood — Flask is still a WSGI framework. True async concurrency requires ASGI + Quart or moving to FastAPI. The runtime-level async/sync decision lives in `mir-backend-python`; here the note is only that Flask's `async def` views do not give you a free event loop.
### 6. Config and secrets — debug mode is an RCE surface
Flask's Werkzeug debugger exposes an interactive Python console in the browser when `DEBUG=True` and an unhandled exception occurs. The console is PIN-protected in recent Werkzeug versions, but the PIN is derivable from `/proc/self/cgroup` and related system files on many Linux hosts. Running `app.run(debug=True)` in production is a remote code execution vulnerability.
**Rules:**
- `DEBUG` is `False` in production, always. Derive it from env: `app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", "0") == "1"`.
- `SECRET_KEY` must be a long, random, secret value — it signs session cookies. A weak or committed key lets an attacker forge session cookies and impersonate any user. Generate once: `python -c "import secrets; print(secrets.token_hex(32))"`, store in a secret manager or environment variable, never in source.
- Load config from environment variables or a secret manager; never hardcode credentials in `config.py` that is committed. Use `python-dotenv` for local dev only, and `.gitignore` the `.env` file.
```python
# WRONG — RCE surface in prod
if __name__ == "__main__":
app.run(debug=True)
# RIGHT — read from environment
app.config.update(
DEBUG=os.environ.get("FLASK_DEBUG", "0") == "1",
SECRET_KEY=os.environ["SECRET_KEY"],
SQLALCHEMY_DATABASE_URI=os.environ["DATABASE_URL"],
)
```
### 7. Migrations via Flask-Migrate / Alembic — same populated-table discipline
Flask-Migrate wraps Alembic. The headline migration safety rules from the FastAPI/Alembic module apply without change: AI writes migrations as if the table is empty; it isn't.
**Key rules for Flask/Alembic:**
- Adding a `NOT NULL` column with a server default on a large table issues a full table rewrite under Postgres < 11, and a locking `UPDATE` under Postgres 11+ if Django-style ORMs generate it. Split into: add nullable → batched `UPDATE` in a `op.execute` data migration → `ALTER COLUMN … SET NOT NULL`.
- Create indexes with `op.create_index(..., postgresql_concurrently=True)` inside a migration that sets `down_revision` correctly and runs outside a transaction (`with op.get_context().autocommit_block():` or via `--no-transaction` in raw Alembic).
- **Separate schema and data migrations.** A data migration holding rows in a lock accumulates schema locks behind it.
- **Reversibility:** provide a `downgrade()` function. If reversal is destructive or impossible, document it explicitly in a comment; don't leave a silent no-op.
```python
# alembic migration — safe NOT NULL add
def upgrade():
op.add_column("order", sa.Column("region", sa.String(), nullable=True))
# data migration in a separate file, then:
def upgrade_set_not_null():
op.alter_column("order", "region", nullable=False)
```
## How this slots into the pipeline
- **Gate 5 (Design):** state the context push strategy for background work, the session teardown pattern, the app-factory shape, and the validation layer (Pydantic/marshmallow). Migration plans for populated tables must name the three-step add-nullable → backfill → NOT NULL pattern.
- **Gate 6 (Implementation):** code against the footguns above; confirm `SECRET_KEY` and `DEBUG` come from env; confirm `db.session.remove()` is in `teardown_appcontext`; confirm background tasks are Celery/RQ, not inline.
- **Gate 7 (Review):** the reliability-reviewer checks items 1–7 here. The migration-reviewer checks each new Alembic revision for NOT NULL columns, missing `CONCURRENTLY`, and missing batched backfills.
## Edit boundary (what belongs here vs. the runtime tier)
**This module holds ONLY Flask library mechanics.** Apply the 3-tier placement test before adding anything:
- True for Go/Node/Java too (idempotency, invariants, gates, risk register, observability)? → **generic core** (`mir-backend`).
- True for every Python framework on CPython (GIL, async-vs-sync, blocking the event loop, fork-safe pools, cold start, asyncio task hygiene)? → **runtime tier** (`mir-backend-python`). Note: the WSGI-vs-ASGI deployment decision lives there; here we only cover Flask-specific async semantics.
- A mechanical footgun of *this library* (context proxies, session scoping, app factory, `debug=True` RCE, `SECRET_KEY`, Flask-Migrate discipline)? → **here**.
- A *different* framework on Python (FastAPI, Django) → its own `mir-backend-python-<framework>` module. A *different* runtime → its own tier. Never widen this one.More Debugging skills
diagnosing-bugs
mattpocock/skills
Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
explore-code
lllllllama/rigorpilot-skills
Rigor Improve implementation leaf skill for auditable candidate implementation in deep learning research repositories. Use when the researcher explicitly authorizes exploratory work on an isolated branch or worktree to transplant modules, adapt a backbone, add LoRA or adapter layers, replace a head, or stitch together meaningful low-risk migration ideas with rollback-aware records in `explore_outputs/`. Do not use for end-to-end exploration orchestration on top of `current_research`, trusted baseline reproduction, conservative debugging, environment setup, verified contribution claims, or default repository analysis.
safe-debug
lllllllama/rigorpilot-skills
Rigor Debug / Rigor Audit skill for deep learning research work. Use when the user pastes a traceback, terminal error, CUDA OOM, checkpoint load failure, shape mismatch, NaN loss symptom, or training failure and wants conservative diagnosis before any patching, with debug fixes clearly separated from research contributions. Do not use for broad refactoring, speculative adaptation, automatic exploratory patching, or general repository familiarization.

