FAPI006 lifespan-events¶
What it does¶
Opt-in. Reports the deprecated startup and shutdown events of a FastAPI app or router:
- Deprecated (warning):
@app.on_event("startup"),@app.on_event("shutdown"),app.add_event_handler(...)andFastAPI(on_startup=..., on_shutdown=...)(the same on anAPIRouter). FastAPI replaces them with thelifespan=context manager. - Never runs (error): the same registration on an app that also sets
lifespan=, or on a router that an app with alifespan=includes. FastAPI runs only the lifespan then, and the handlers are silently ignored.
The finding sits on the registration: the @app.on_event(...) decorator, the add_event_handler call, or the on_startup= / on_shutdown= keyword. The receiver can be in another file: a handler in app/events.py is matched to the app in app/main.py through the project's imports.
Why is this bad¶
The FastAPI docs say it plainly: "It's all lifespan or all events, not both." An agent that adds @app.on_event("startup") to an app that already has a lifespan= writes a handler that never runs. Nothing fails at startup, the connection pool is never opened, and the first request is the one that finds out. Ruff has no rule for it.
Example¶
[tool.inwards]
layers = [{ name = "app", modules = ["app"] }]
[tool.inwards.rules]
extend-select = ["FAPI006"]
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
app = FastAPI(lifespan=lifespan)
from app.main import app
@app.on_event("startup")
async def connect() -> None:
print("connecting")
app/events.py:4:1: FAPI006 `@app.on_event("startup")` never runs: `app` (app/main.py:11) sets `lifespan=`, and FastAPI then ignores event handlers. It's all lifespan or all events, not both.
fix: Move the handler into the lifespan context manager.
1. Move what this handler does into the lifespan function that is already set.
2. Write one async lifespan function that takes the app, with the startup code before its `yield` and the shutdown code after it, wrap it with `contextlib.asynccontextmanager`, and pass it as `lifespan=`.
3. Delete the old handler only after its body has moved: don't remove it to make this finding go away, as its startup or shutdown work would be lost.
docs: https://sircypkowskyy.github.io/inwards/rules/FAPI006/
Fixed: the startup code moves into the lifespan, before the yield.
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
print("connecting")
yield
app = FastAPI(lifespan=lifespan)
How it works¶
FAPI006 reads the shared FastAPI model (ADR-037): each on_event decorator and add_event_handler call is recorded with the app or router it is called on, and the receiver is resolved to its FastAPI(...) or APIRouter(...) call, in this file or another one.
- A registration on an object whose constructor passes
lifespan=(anything butNone) is an error. - A registration on a router is also an error when an app, or a router under one, that includes it passes
lifespan=. The inclusions come from the app and router graph of FAPI003. - Any other registration, and any
on_startup=/on_shutdown=that isn't an empty list orNone, is a warning.
Where it runs:
inwards check,inwards baselineand the Stop gate report all three.- The PostToolUse hook and the editor don't read the router graph: a router's handler is a warning there, and the Stop gate raises it to an error when an app above it has a lifespan.
How to fix¶
- Write one
@asynccontextmanagerfunction that takes the app: the startup code goes before itsyield, the shutdown code after it. - Pass it as
FastAPI(lifespan=...), and delete theon_eventhandlers andon_startup=/on_shutdown=lists once their bodies have moved. - Don't delete a handler to silence the finding: the work it does at startup or shutdown would be lost.
Fix safety¶
No automatic fix (autofix: false). Merging handlers into a lifespan changes the order startup and shutdown code runs in, which is a decision for the author.
Configuration¶
Severity error, with the deprecated-only findings reported as warnings. Opt-in: turn it on with extend-select. It has no options of its own; modules works as for every rule, and check-tests as for every content rule (options tables).
Every finding can be suppressed inline with # inwards: ignore[FAPI006] reason="..." on the line of the registration.
Known limitations¶
Unknown means silent for the error: a registration stays a warning when
- the app's constructor passes
**kwargsthat may hide alifespan=; - an
include_routerin the project can't be followed, so the router's parents are unknown.
More limits:
- A receiver Inwards can't resolve to a
FastAPI(...)orAPIRouter(...)call, such as an app a function receives as a parameter, isn't reported at all. - Starlette apps (
Starlette(on_startup=...)) aren't read, onlyFastAPIandAPIRouter. - A lifespan set after construction (
app.router.lifespan_context = ...) isn't seen. - A file that doesn't mention FastAPI is read only if it spells
on_eventoradd_event_handler.
References¶
- ADR-037: the FastAPI family, opt-in, and the shared model.
- FastAPI docs, Lifespan Events: "It's all lifespan or all events, not both."