Setup before the handler, teardown after it - the shape a database session needs.
Overview
The shape
A dependency with yield is one that does work before your handler runs and again after it finishes. The code before the yield is setup. The value after the yield is what the handler receives. The code after the yield is cleanup. It is how you open a database session, hand it to the handler, and close it afterward.
You need this whenever a resource must be released. A connection left open, a transaction left half done, or a file never closed will cause trouble that appears later and far from its cause. Putting the cleanup next to the setup keeps the pair together, so neither is forgotten.
This article starts with the basic shape and what each part does. It then explains a key promise: teardown always runs, even when the handler raises an error. That guarantee is what makes the pattern safe. You will also see how that interacts with exceptions and what the cleanup code can and cannot do about them.
A practical example follows, the transaction pattern. The session commits if the handler succeeds and rolls back if it fails, all in one small function. This is the form you will use most.
The article closes with nesting and order, which tells you which cleanup runs first when several dependencies stack up, and with how caching applies so the same resource is not opened twice in one request.
If you have used Python's context managers, the idea will feel familiar. It is the same pattern of setup, use, and cleanup, adapted to the life of a single request. Reading this article will also make several later topics easier to follow, including testing and lifespan events. Each section is short and ends with a habit you can apply straight away. You finish with a reliable way to manage database sessions, files, and other resources, and with a clear sense of what happens when something fails halfway through a request.
def get_session():
db = Session()
try:
yield db
finally:
db.close()
all before the yield runs before the handler. The yielded value is what the handler receives. all after runs once the response has been made.
It's a gen, and it behaves like a context manager. Which is just the point. "acquire, use, release" is what a DB session, a file handle, a lock or a temporary folder all need.
Worth knowing
Code before yield is setup, the yielded value is what the handler receives, and code after it is teardown.
Teardown runs whether the handler returned or raised, which is why try/finally belongs around the yield.
Putting commit() immediately after the yield and rollback() in an except gives the transaction pattern for free.
With several yield dependencies, teardowns run in reverse order — last opened, first closed.
They are cached per request like any dependency: two references share one setup and one teardown.
Do not raise in the teardown. The response is already being built, so the error cannot reach the caller cleanly — catch and log it instead.
Dependencies with yield
Setup before the handler, teardown after it, and the transaction pattern that falls out.
yield splits it in two
Everything before the yield runs first, the handler gets the yielded value, and everything after runs when the response is done.
Two references to a yield dependency share one setup and one teardown, the same as any other.
example_05.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
log = []
def session():
log.append("open")
try: yield "s%d" % log.count("open")
finally: log.append("close")
def repo(s: str = Depends(session)):
return "repo(%s)" % s
@app.get("/x")
def x(a: str = Depends(session), b: str = Depends(repo)):
return {"a": a, "b": b}
print(TestClient(app).get("/x").json())
print("log:", log)
print()
print("One open, one close - even though two parameters wanted it.")
Output
What not to do in the teardown
Raising after the yield happens once the response is already being built, so it cannot become a clean error for the caller.
example_06.pyFastAPI
from fastapi import Depends, FastAPI
app = FastAPI()
log = []
def risky():
log.append("open")
try:
yield "value"
finally:
log.append("close")
# Anything that can fail here should be caught here.
try:
raise RuntimeError("cleanup failed")
except RuntimeError as e:
log.append("swallowed: %s" % e)
@app.get("/x")
def x(v: str = Depends(risky)):
return {"v": v}
r = TestClient(app).get("/x")
print("status:", r.status_code, r.json())
print("log :", log)
print()
print("The response was already decided. Log the failure; do not raise it.")
Output
Teardown always runs
The reason this exists rather than a plain dependency returning a session: cleanup must happen even when the handler fails.
Without try/finally, a handler that raises a 404 would skip the close and leak the link. With it, the session is returned to the pool either way. In an use handling real traffic, that gap is the gap between a pool that stays healthy and one that is exhausted an hour after deploy.
The rule is simple: if a yield dependency acquires anything, the yield belongs inside a try. The release belongs in finally.
The transaction pattern
Because the teardown can see whether the handler raised, the standard DB shape falls out on its own:
def get_db():
db = Session()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
commit() sits at once after the yield. It only runs if the handler done. Any exception. Including an HTTPException. Rolls back and re-raises. close() runs even so.
Three lines, and each endpoint using this dependency now has correct transaction handling without writing any. That is a large amount of correctness for very little code, and it's the single most common use of the feature.
Note the raise in the except. Swallowing there would turn a failed request into a good-looking one with a rolled-back transaction. This Is worse than either outcome alone.
Nesting and order
A yield dependency can depend on another. Setups run outermost first, teardowns run in reverse. The same discipline as nested with blocks. For the same reason. The inner thing may need the outer one to still exist while it closes.
The fourth editor above prints the whole sequence. It's worth running once. The sorting is the sort of thing that is obvious when you see it and easy to get backwards when thinking about it.
Caching applies
A yield dependency is cached per request like any other. Two params wanting the same session get one session, opened once and closed once.
That matters for correctness rather than just speed. Two split sessions in one request would mean two transactions. A handler that read through one and wrote through the other would give results no one could explain.
What not to do in teardown
Don't raise. By the time the teardown runs, the response has been decided and is being sent. An exception there can't become a clean error for the caller. Based on where it happens it may truncate the response or surface as a server error unrelated to anything the client did.
If cleanup can fail, catch it and log it. The request already succeeded or failed on its own terms. A cleanup problem is an working issue rather than a thing the caller can act on.
Don't do slow work. The teardown runs after the handler. For a synchronous dependency it's on the request path. A long-running cleanup delays the response for no gain to the caller. Anything truly slow belongs in a background task.
Don't lean on the exception being visible. In older FastAPI versions the exception wasn't always on hand to the teardown in the way people expected. The try/except/finally shape above works because it wraps the yield right rather than trying to inspect state.
What belongs here
Anything with a lifetime tied to the request: DB sessions, transactions, file handles, temporary folders, locks, an HTTP client that should be closed.
What doesn't: work that could be done once at startup. A link *pool* is created at startup and lives for the process. A *session* is taken from it per request. Confusing the two makes either a pool rebuilt on each request or a session shared between them. Both are bad in other ways.
That gap is the lifespan module's subject.
Mistakes people make with Dependencies with yield
**No try/finally.** The single most consequential omission. A handler that raises then skips the cleanup. A link leaks on just the requests you most want to live.
**Committing in finally.** It runs on failure too. A rolled-back-looking request quietly commits. Commit belongs at once after the yield. Here Only success reaches it.
Swallowing the exception. An except that rolls back and doesn't re-raise turns a failed request into one that looks good with nothing written.
Raising in the teardown. The response is already being built. The error can't reach the caller cleanly and may truncate what was being sent.
Slow cleanup. The teardown is on the request path. Anything truly slow belongs in a background task.
Creating the pool per request. A pool is startup work with a process lifetime. A session is request work. Rebuilding the pool per request is catastrophic for throughput. Sharing a session across requests is catastrophic for correctness.
Sync or async
Both forms work, and the choice follows the resource.
def get_session() runs in the threadpool. This Is right for a blocking driver - psycopg2, a synchronous HTTP client, a file.
async def get_session() runs on the event loop. This Is right for an async driver - asyncpg, httpx's async client.
Mixing is allowed: an async handler may depend on a sync yield dependency and the other way round. What matters is that a blocking call isn't made right on the loop. This Is the runtime tier's subject.
How it works underneath
Knowing the method drops most of the surprises.
A yield dependency is a gen. FastAPI wraps it in a context manager and enters it before calling the handler, holding it open in an AsyncExitStack that lives for the request. When the response has been made, the stack unwinds and each context manager exits in reverse order.
That is why teardown sorting is reverse, why teardown runs on failure. Why raising in teardown is awkward. The stack is unwinding while the response is already on its way out.
It also shows the lifetime just. The dependency is alive for the whole request, including while the response is being serialised - so a session yielded here is still usable by a response model reading lazy fields. This Is a common source of confusion when it's *not* the case in other frameworks.
A checklist
Before shipping a yield dependency, four questions.
Does it acquire a thing? Then the yield is inside a try and the release is in finally.
Can the handler fail? Then anything gated on success sits between the yield and the except.
Can the cleanup fail? Then it's caught and logged, not raised.
Is this per-request, or per-process? A session is per request. The pool it comes from isn't.
One more thing to watch
A subtle one, worth knowing before it bites.
The dependency stays open while the response is serialised, not just while the handler runs. A session yielded here is still alive when a response model reads an field that triggers a lazy load.
That is convenient, and it's also how a single output quietly turns fifty queries. The model touches a link, the session is still open, and the ORM obliges. The fix is on the query side - load what the response needs up front - but the reason it's possible at all is this lifetime.
It's also why closing the session inside the handler is a mistake that appears to work. The handler returns fine. The serialiser then finds a closed session, and the error names neither.
Dependencies with yield: summary
Code before the yield is setup, the yielded value is what the handler receives. Code after it runs once the response has been made - on success and on failure alike.
Wrap the yield in try/finally when anything is acquired. Put commit() at once after it and rollback() in an except that re-raises. The transaction pattern falls out in three lines.
Teardowns run in reverse order, the per-request cache still applies. Nothing in the teardown should raise or be slow.
Summary, in one line
Setup before the yield, teardown after it, try/finally around it when anything is acquired - and commit() at once after the yield so that only a good handler reaches it.
all else in this module is a result of those four facts.
A closing thought
Almost each resource bug in a web use is a lifetime bug. A thing opened and not closed, closed too early, or shared between requests that shouldn't share it.
yield dependencies exist to make the common lifetime - one request - the easy one to express. Setup, use, teardown, in one function, applied by declaring a param.
The failure modes it drops are the ones that don't show up in development. A link leaked on the error path, a transaction left open, a pool exhausted an hour after deploy under real traffic.
Two rules
all here reduces to two.
**Wrap the yield in try/finally when the dependency acquires anything.** That single line is the gap between a pool that lives a bad afternoon and one that doesn't.
**Put success-only work between the yield and the except.** That is where commit() goes. It's why the transaction pattern needs no flag, no inspection of the response. No cooperation from the handler.
Where it sits in the tier
Of the five modules here, this is the one whose lack causes production incidents rather than inconvenience.
A missing Depends is a repeated four lines. A missing try/finally is a link pool that empties under load on the error path. This Is the path that gets busy just when all else is going wrong.
Questions people ask about Dependencies with yield
What does yield do in a dependency? Code before yield runs before the endpoint, the yielded value is injected, and code after yield runs once the response is handled.
Does the cleanup run if the endpoint raises? Yes, if you put it in a finally block. Without finally, an exception skips code after yield.
What is the common use? Database sessions and other resources: open a session, yield it, and close it afterward.
In what order do nested yield dependencies close? In reverse order of creation, so the dependency opened last closes first.
Should I raise HTTPException in the teardown? Not after yield. The response may have already been sent, so errors there cannot change the status.
Does a sync generator dependency block the loop? No. FastAPI runs sync generators in a thread pool, while async generators run on the event loop.
Where to go after Dependencies with yield
Dependencies that depend on dependencies, the tree FastAPI resolves before your handler runs. Why a shared node in that tree is still only called once.
Check yourself
0 of 4
Answer without scrolling back up.
When does the code after `yield` run?
Setup, then handler, then teardown - which is why it works for anything with a request-scoped lifetime.
A handler raises a 404. Does the teardown run?
That is the reason to use yield rather than a plain return. Without `try/finally` a failing handler skips the close and leaks the connection.
Where does `db.commit()` belong in the transaction pattern?
It is only reached when the handler completed without raising. An `except` clause rolls back and re-raises; `finally` closes either way.
Cleanup itself fails. What should the teardown do?
The response is already decided and being sent, so an exception there cannot become a clean error - and may truncate the response instead.
Cheat sheet
Dependencies with yield
Write a dependency that yields a database session, watch FastAPI run setup before the handler and teardown after it, then see the pattern.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.