Skip to main content
A context manager is an object that automatically performs setup before a block of code runs and cleanup after it finishes — even if an exception occurs. You’ve almost certainly used one without realising it: with open("file.txt") as f: is Python’s most common context manager. Understanding how they work under the hood helps you write safer resource-management code and opens the door to understanding how FastAPI’s dependency injection and lifespan events operate.

The Most Common Context Manager

What happens step by step:
  1. open("students.txt") creates a file object.
  2. Python calls file.__enter__(), which returns the file object assigned to file.
  3. The code inside the with block executes.
  4. When execution leaves the block (normally or via an exception), Python automatically calls file.__exit__(), which closes the file.
You never need to write file.close() — the context manager guarantees it.

How with Works

Any object used with with must implement two special methods:
  • __enter__() — performs setup; its return value is bound to the as variable.
  • __exit__(exc_type, exc_value, traceback) — performs cleanup; receives exception info if one occurred.
Conceptually, this block:
works like:

Creating a Class-Based Context Manager

The variable after as receives whatever __enter__() returns.

Generator-Based Context Managers

Writing a full class for simple set-up/tear-down is often unnecessary. Use the @contextmanager decorator from contextlib instead:

Understanding yield

The yield divides the generator into two phases:
  • Before yield — runs when entering the with block (setup).
  • The yielded value — becomes the variable after as.
  • After yield — runs when leaving the with block (cleanup).

Exception Handling Inside the Generator

When an exception escapes the with block, Python does not call next(generator) to resume it normally. Instead it calls generator.throw(exception), injecting the exception back at the point where the generator was paused.
"After Yield" never prints because the exception was injected at the yield, jumping straight to the except block.

Complete Walk-Through

Execution flow:
1

Generator is created

generator = my_context() — nothing executes yet.
2

Python enters the context

next(generator) runs until yield data. context_data = "my data".
3

with block executes

print(context_data) runs, then raise Exception("error").
4

Context finally runs

"in context finally" is printed before the exception propagates.
5

generator.throw() is called

Python injects the exception back into the generator at the suspended yield.
6

Generator handles the exception

The except block catches it and prints "from generator: error".
7

Generator finally runs

"in generator finally" is always printed.

FastAPI Uses the Same Mechanism

FastAPI’s yield-based dependencies use exactly this pattern:
FastAPI resumes the generator after the endpoint function returns, ensuring cleanup always runs before the HTTP response is sent to the client.

Key Takeaways

  • A context manager performs setup and cleanup automatically.
  • The with statement works with any object implementing __enter__() and __exit__().
  • The variable after as receives the return value of __enter__() (or the yielded value for generator-based managers).
  • @contextmanager is the concise way to create custom context managers using a generator function.
  • Normal completion resumes the generator with next(generator).
  • Exceptions resume the generator with generator.throw(exception).
  • Code after yield only runs during normal (non-exception) exit.
  • finally always runs — making it ideal for guaranteed resource clean-up.
  • FastAPI yield dependencies are built on this exact mechanism.