Dependencies with yield and scopes

FastAPI · Advanced · 14 min

Sessions, transactions, locks and file handles need reliable cleanup, including when the endpoint raises.

A dependency that yields works like a context manager: code before yield is setup, the yielded value is injected, and code after yield is teardown. FastAPI wraps it with contextlib.contextmanager (or asynccontextmanager for async def) and enters it on an exit stack, so teardowns run in reverse order of setup. Sync generator dependencies run both halves in the threadpool.

Exceptions flow into the dependency: if the endpoint or a later dependency raises, the exception is re-raised at the yield. That enables a transaction pattern:

def get_tx_session():
    with Session(engine) as session:
        try:
            yield session
            session.commit()
        except Exception:
            session.rollback()
            raise

Always re-raise in an except block, or raise a new HTTPException. If you swallow the exception, the client still gets a 500 but nothing is logged, which is miserable to debug. Raising HTTPException before the yield is fine too, for example when a lock can't be acquired.

When does teardown run? Since FastAPI 0.118 the default, scope="request", runs it after the response has been sent, so the resource stays alive while a StreamingResponse streams. (From 0.106 to 0.117 it ran before sending.) The catch: a commit after yield happens after the client already received its 200, so a failing commit can't change the response. Two fixes:

  • commit explicitly in the endpoint before returning, or
  • since 0.121, declare Depends(get_tx_session, scope="function"), which runs teardown as soon as the endpoint function returns, before the response is sent.

A "function"-scoped dependency may depend on "request"-scoped ones but not the reverse, because outer resources must outlive inner ones.

Example

from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient

events: list[str] = []

def transaction():
    events.append("begin")
    try:
        yield "tx"
        events.append("commit")
    except Exception:
        events.append("rollback")
        raise

app = FastAPI()
Tx = Annotated[str, Depends(transaction, scope="function")]

@app.post("/ok")
def ok(tx: Tx):
    events.append("work")
    return {"done": True}

@app.post("/fail")
def fail(tx: Tx):
    raise HTTPException(status_code=409, detail="conflict")

c = TestClient(app)
c.post("/ok"); c.post("/fail")
print(events)   # ['begin', 'work', 'commit', 'begin', 'rollback']

Remember

  • Code before yield is setup, after yield is teardown, in reverse order
  • Exceptions from the endpoint are re-raised at the yield: always re-raise
  • Default scope='request' tears down after the response is sent (0.118+)
  • scope='function' (0.121+) tears down before the response, so commit errors can still fail the request

Watch out for

  • Catching an exception after yield and not re-raising it, which hides the error from logs.

Check yourself

  1. With the default scope, a dependency commits after yield and the commit fails. What has the client already received?
  2. Which dependency arrangement is invalid?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/