> ## Documentation Index
> Fetch the complete documentation index at: https://genai.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Context Managers in Python

> Learn how context managers work, how to create your own using classes and generators, and how FastAPI uses the same mechanism for dependencies.

# Context Managers

A **context manager** is an object that performs:

* **Setup** before a block of code executes.
* **Cleanup** after the block finishes, even if an exception occurs.

It is used with the `with` statement.

```python theme={null}
with resource:
    # use the resource
```

General lifecycle:

```text theme={null}
Acquire Resource
      │
      ▼
Execute Block
      │
      ▼
Release Resource
```

Common resources managed using context managers:

* Files
* Database connections
* Locks
* Network sockets
* Temporary directories

# The Most Common Context Manager

Most Python developers use a context manager without realizing it.

```python theme={null}
with open("students.txt") as file:
    print(file.read())
```

Let's understand what happens.

## Step 1

```python theme={null}
open("students.txt")
```

creates a **file object**.

## Step 2

The returned object is assigned to the variable after `as`.

```python theme={null}
with open("students.txt") as file:
```

Conceptually,

```python theme={null}
file = open("students.txt")
```

The variable **`file`** holds the object returned by `open()`.

## Step 3

The code inside the block executes.

```python theme={null}
print(file.read())
```

## Step 4

When execution leaves the `with` block, Python automatically closes the file.

```text theme={null}
Create File
     │
     ▼
Assign to file
     │
     ▼
Execute Block
     │
     ▼
Close File
```

# How Does `with` Work?

Any object used with the `with` statement must implement two special methods.

```python theme={null}
__enter__()
__exit__()
```

Conceptually,

```python theme={null}
with resource as value:
    print(value)
```

works like this:

```python theme={null}
resource = ...

value = resource.__enter__()

try:
    print(value)
finally:
    resource.__exit__()
```

* `__enter__()` performs setup.
* `__exit__()` performs cleanup.

# Creating a Context Manager Using a Class

```python theme={null}
class MyContext:
    def __enter__(self):
        print("Setup")
        return "Hello"

    def __exit__(self, exc_type, exc_value, traceback):
        print("Cleanup")
```

Usage:

```python theme={null}
with MyContext() as message:
    print(message)
```

Output:

```text theme={null}
Setup
Hello
Cleanup
```

Notice that the variable after `as` receives the value returned by `__enter__()`.

# Generator-Based Context Managers

Writing a class for simple setup and cleanup is often unnecessary.

Python provides the `@contextmanager` decorator.

```python theme={null}
from contextlib import contextmanager
```

Example:

```python theme={null}
from contextlib import contextmanager

@contextmanager
def my_context():
    print("Setup")
    yield "Hello"
    print("Cleanup")
```

Usage:

```python theme={null}
with my_context() as message:
    print(message)
```

Output:

```text theme={null}
Setup
Hello
Cleanup
```

# Understanding `yield`

The `yield` divides the function into two parts.

Before `yield`

```python theme={null}
print("Setup")
```

runs before entering the `with` block.

The yielded value

```python theme={null}
yield "Hello"
```

becomes the variable after `as`.

```python theme={null}
message = "Hello"
```

After the `with` block finishes, execution resumes after the `yield`.

```python theme={null}
print("Cleanup")
```

Execution flow:

```text theme={null}
Generator Starts
       │
       ▼
Setup
       │
       ▼
yield "Hello"
       │
       ▼
Execute with Block
       │
       ▼
Resume Generator
       │
       ▼
Cleanup
```

# Normal Execution

Example:

```python theme={null}
from contextlib import contextmanager

@contextmanager
def my_context():
    print("Setup")
    yield "Hello"
    print("Cleanup")

with my_context() as message:
    print(message)
```

Output:

```text theme={null}
Setup
Hello
Cleanup
```

Conceptually, Python behaves like:

```python theme={null}
generator = my_context()

message = next(generator)

print(message)

next(generator)
```

Notice that the generator is resumed using:

```python theme={null}
next(generator)
```

which continues execution after the `yield`.

# Exception Handling

Now consider this example.

```python theme={null}
from contextlib import contextmanager

@contextmanager
def my_context():
    try:
        yield "Hello"
        print("After Yield")
    finally:
        print("Cleanup")

with my_context() as message:
    print(message)
    raise Exception("Something went wrong")
```

Output:

```text theme={null}
Hello
Cleanup
Traceback...
```

Notice that:

```python theme={null}
print("After Yield")
```

never executes.

Why?

Because Python does **not** resume the generator normally.

Instead of:

```python theme={null}
next(generator)
```

Python resumes it using:

```python theme={null}
generator.throw(exception)
```

The exception is injected back into the generator exactly where it was paused.

Conceptually,

```python theme={null}
yield "Hello"
```

becomes

```python theme={null}
raise Exception("Something went wrong")
```

Execution flow:

```text theme={null}
yield
   │
   ▼
Exception inside with
   │
   ▼
generator.throw(exception)
   │
   ▼
finally
```

# Catching the Exception Inside the Generator

The generator itself can handle the exception.

```python theme={null}
from contextlib import contextmanager

@contextmanager
def my_context():
    try:
        yield "Hello"
        print("After Yield")
    except Exception as e:
        print("Generator caught:", e)
    finally:
        print("Cleanup")

with my_context() as message:
    print(message)
    raise Exception("Error")
```

Output:

```text theme={null}
Hello
Generator caught: Error
Cleanup
```

Notice that:

```python theme={null}
print("After Yield")
```

is skipped.

Since the exception was thrown back into the generator, execution jumps directly to:

```python theme={null}
except
```

instead of continuing after the `yield`.

# Complete Example

The following example demonstrates how an exception raised inside the `with` block travels back into the generator.

```python theme={null}
from contextlib import contextmanager

@contextmanager
def my_context():
    try:
        data = "my data"
        yield data
        print("after yield")
    except Exception as e:
        print("from generator:", e)
    finally:
        print("in generator finally")

with my_context() as context_data:
    try:
        print(context_data)
        raise Exception("error")
    finally:
        print("in context finally")
```

Output:

```text theme={null}
my data
in context finally
from generator: error
in generator finally
```

## Step 1

Python creates the generator.

```python theme={null}
generator = my_context()
```

Nothing executes yet.

## Step 2

Python enters the context.

```python theme={null}
context_data = next(generator)
```

Execution pauses at:

```python theme={null}
yield data
```

The yielded value becomes:

```python theme={null}
context_data = "my data"
```

## Step 3

The `with` block executes.

```python theme={null}
print(context_data)
raise Exception("error")
```

Before leaving the block, the local `finally` executes.

```text theme={null}
in context finally
```

The exception is still active.

## Step 4

Since an exception escaped from the `with` block, Python does **not** call:

```python theme={null}
next(generator)
```

Instead it calls:

```python theme={null}
generator.throw(Exception("error"))
```

The exception is injected back into the generator at the suspended `yield`.

Conceptually,

```python theme={null}
yield data
```

becomes

```python theme={null}
raise Exception("error")
```

inside the generator.

## Step 5

The generator catches the exception.

```python theme={null}
except Exception as e:
    print("from generator:", e)
```

Output:

```text theme={null}
from generator: error
```

Notice that:

```python theme={null}
print("after yield")
```

never executes because execution does not continue normally after the `yield`.

## Step 6

Finally always executes.

```python theme={null}
finally:
    print("in generator finally")
```

Output:

```text theme={null}
in generator finally
```

Since the generator catches the exception and does **not** re-raise it, the exception is considered handled and no traceback is produced.

Execution flow:

```text theme={null}
Create Generator
       │
       ▼
next(generator)
       │
       ▼
yield "my data"
       │
       ▼
Execute with Block
       │
       ▼
Exception Raised
       │
       ▼
Context Finally
       │
       ▼
generator.throw(exception)
       │
       ▼
Generator except
       │
       ▼
Generator finally
       │
       ▼
Generator ends
```

# FastAPI Uses the Same Mechanism

FastAPI uses generator-based context managers for dependencies.

```python theme={null}
from fastapi import Depends, FastAPI

app = FastAPI()

def get_message():
    print("Creating resource")
    try:
        yield "Hello"
    finally:
        print("Cleaning resource")

@app.get("/")
def home(message: str = Depends(get_message)):
    return {"message": message}
```

Execution flow:

```text theme={null}
Request
    │
    ▼
Call Dependency
    │
    ▼
yield value
    │
    ▼
Execute Endpoint
    │
    ▼
Endpoint Returns
    │
    ▼
Resume Generator
    │
    ▼
Cleanup
    │
    ▼
Send Response
```

The endpoint returning does **not** immediately send the HTTP response.

FastAPI first resumes the generator so that cleanup executes, and only then sends the response to the client.

# Key Takeaways

* A context manager automatically performs setup and cleanup.
* The `with` statement works with objects implementing `__enter__()` and `__exit__()`.
* The variable after `as` receives the object returned by `__enter__()` or yielded by the generator.
* `@contextmanager` provides a concise way to create custom context managers.
* Normal completion resumes the generator using `next(generator)`.
* Exceptions resume the generator using `generator.throw(exception)`.
* `generator.throw()` injects the exception at the suspended `yield`.
* Statements after `yield` execute only during normal execution.
* `finally` always executes, making context managers ideal for resource management.
* FastAPI's `yield` dependencies are built on the same mechanism.
