Setup before the handler, teardown after it - the shape a database session needs.
Overview
The shape
def get_session():
db = Session()
try:
yield db
finally:
db.close()
Everything before the yield runs before the handler. The yielded value is what the handler receives. Everything after runs once the response has been produced.
It is a generator, and it behaves like a context manager — which is exactly the point, because "acquire, use, release" is what a database session, a file handle, a lock or a temporary directory 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.
example_01.pyFastAPI
Output
Teardown runs even on failure
That is the whole point. A session opened before the handler is closed whether the handler returned or raised.
example_02.pyFastAPI
Output
Commit on success, roll back on failure
The pattern this exists for: the teardown can see whether the handler raised, and decide what to do about it.
example_03.pyFastAPI
Output
Order with several of them
Teardowns run in reverse, like nested context managers — the last thing opened is the first thing closed.
example_04.pyFastAPI
Output
It is still cached per request
Two references to a yield dependency share one setup and one teardown, the same as any other.
example_05.pyFastAPI
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
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 connection. With it, the session is returned to the pool either way. In an application handling real traffic, that difference 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, and the release belongs in finally.
The transaction pattern
Because the teardown can observe whether the handler raised, the standard database shape falls out naturally:
def get_db():
db = Session()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
commit() sits immediately after the yield, so it only runs if the handler completed. Any exception — including an HTTPException — rolls back and re-raises. close() runs regardless.
Three lines, and every 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 is the single most common use of the feature.
Note the raise in the except. Swallowing there would turn a failed request into a successful-looking one with a rolled-back transaction, which 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, and 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 is worth running once, because the ordering is the sort of thing that is obvious when you see it and easy to get backwards when reasoning about it.
Caching applies
A yield dependency is cached per request like any other. Two parameters wanting the same session get one session, opened once and closed once.
That matters for correctness rather than just performance: two separate sessions in one request would mean two transactions, and a handler that read through one and wrote through the other would produce results nobody could explain.
What not to do in teardown
Do not raise. By the time the teardown runs, the response has been decided and is being sent. An exception there cannot become a clean error for the caller, and depending 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, and a cleanup problem is an operational issue rather than something the caller can act on.
Do not do slow work. The teardown runs after the handler, and for a synchronous dependency it is on the request path. A long-running cleanup delays the response for no benefit to the caller. Anything genuinely slow belongs in a background task.
Do not rely on the exception being visible. In older FastAPI versions the exception was not always available to the teardown in the way people expected. The try/except/finally shape above works because it wraps the yield directly rather than trying to inspect state.
What belongs here
Anything with a lifetime tied to the request: database sessions, transactions, file handles, temporary directories, locks, an HTTP client that should be closed.
What does not: work that could be done once at startup. A connection *pool* is created at startup and lives for the process; a *session* is taken from it per request. Confusing the two produces either a pool rebuilt on every request or a session shared between them, and both are bad in different ways.
That distinction is the lifespan module's subject.
Mistakes people make
No try/finally. The single most consequential omission. A handler that raises then skips the cleanup, and a connection leaks on exactly the requests you most want to survive.
Committing in finally. It runs on failure too, so a rolled-back-looking request quietly commits. Commit belongs immediately after the yield, where only success reaches it.
Swallowing the exception. An except that rolls back and does not re-raise turns a failed request into one that looks successful with nothing written.
Raising in the teardown. The response is already being built. The error cannot reach the caller cleanly and may truncate what was being sent.
Slow cleanup. The teardown is on the request path. Anything genuinely 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, and 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, which is right for a blocking driver - psycopg2, a synchronous HTTP client, a file.
async def get_session() runs on the event loop, which 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 is not made directly on the loop, which is the runtime tier's subject.
How it works underneath
Knowing the mechanism removes most of the surprises.
A yield dependency is a generator. 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 produced, the stack unwinds and every context manager exits in reverse order.
That is why teardown ordering is reverse, why teardown runs on failure, and why raising in teardown is awkward: the stack is unwinding while the response is already on its way out.
It also explains the lifetime precisely. 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 attributes, which is a common source of confusion when it is *not* the case in other frameworks.
A checklist
Before shipping a yield dependency, four questions.
Does it acquire something? Then the yield is inside a try and the release is in finally.
Can the handler fail? Then anything conditional on success sits between the yield and the except.
Can the cleanup fail? Then it is caught and logged, not raised.
Is this per-request, or per-process? A session is per request; the pool it comes from is not.
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 attribute that triggers a lazy load.
That is convenient, and it is also how a single serialisation quietly becomes fifty queries: the model touches a relationship, 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 is possible at all is this lifetime.
It is 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.
Summary
Code before the yield is setup, the yielded value is what the handler receives, and code after it runs once the response has been produced - on success and on failure alike.
Wrap the yield in try/finally whenever anything is acquired. Put commit() immediately after it and rollback() in an except that re-raises, and the transaction pattern falls out in three lines.
Teardowns run in reverse order, the per-request cache still applies, and nothing in the teardown should raise or be slow.
Summary, in one line
Setup before the yield, teardown after it, try/finally around it whenever anything is acquired - and commit() immediately after the yield so that only a successful handler reaches it.
Everything else in this module is a consequence of those four facts.
A closing thought
Almost every resource bug in a web application is a lifetime bug: something opened and not closed, closed too early, or shared between requests that should not 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 parameter.
The failure modes it removes are the ones that do not show up in development: a connection leaked on the error path, a transaction left open, a pool exhausted an hour after deploy under real traffic.
Two rules
Everything here reduces to two.
Wrap the yield in try/finally whenever the dependency acquires anything. That single line is the difference between a pool that survives a bad afternoon and one that does not.
Put success-only work between the yield and the except. That is where commit() goes, and it is why the transaction pattern needs no flag, no inspection of the response, and no cooperation from the handler.
Where it sits in the tier
Of the five modules here, this is the one whose absence causes production incidents rather than inconvenience.
A missing Depends is a repeated four lines. A missing try/finally is a connection pool that empties under load on the error path, which is the path that gets busy exactly when everything else is going wrong.
Next
Dependencies that depend on dependencies, the tree FastAPI resolves before your handler runs, and 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
Everything before the yield runs before the handler. The yielded value is what the handler receives. Everything after runs once the response has been produced.
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.