# Context Managers

## 1. The `with` Statement — Guaranteed Cleanup

```python
with open("data.txt") as f:
    contents = f.read()
# f is automatically closed here, even if .read() raised an exception
```

`with` guarantees that some cleanup action runs when the block exits — normally, via `return`, or via an exception propagating out — without needing an explicit `try/finally`. `open()` returns a **context manager**: an object that knows how to set something up on entry and tear it down on exit.

## 2. The Protocol: `__enter__` and `__exit__`

```python
class Resource:
    def __enter__(self):
        print("acquiring")
        return self   # becomes the `as` target

    def __exit__(self, exc_type, exc_value, traceback):
        print("releasing")
        return False   # False (or None) = do NOT suppress any exception

with Resource() as r:
    print("using", r)
```

Output:
```
acquiring
using <Resource object ...>
releasing
```

`with EXPR as name:` desugars, roughly, to calling `EXPR.__enter__()` and binding its return value to `name`, running the block, then **always** calling `EXPR.__exit__(exc_type, exc_value, traceback)` on the way out — whether the block finished normally (all three arguments `None`) or an exception is propagating through it (all three populated).

## 3. Suppressing Exceptions via `__exit__`'s Return Value

```python
class SwallowValueError:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is ValueError:
            print(f"suppressed: {exc_value}")
            return True   # True = suppress this exception, block exits normally
        return False        # anything else propagates as usual

with SwallowValueError():
    raise ValueError("boom")   # caught and suppressed by __exit__

print("still running")           # this DOES run — the exception never escaped the with block
```

If `__exit__` returns a truthy value, the exception that was propagating through the `with` block is suppressed entirely — code after the `with` block continues as if nothing happened. Returning `False`/`None` (the default if you don't explicitly return anything) lets the exception propagate normally after cleanup runs. This is deliberate, explicit opt-in behavior — accidentally suppressing exceptions by forgetting to `return False` is a real footgun (see Common Pitfalls).

## 4. `contextlib.contextmanager` — Generator-Based Context Managers

```python
from contextlib import contextmanager

@contextmanager
def resource():
    print("acquiring")
    try:
        yield "the resource"       # this becomes the `as` target
    finally:
        print("releasing")          # ALWAYS runs, exception or not

with resource() as r:
    print("using", r)
```

Writing a full class with `__enter__`/`__exit__` is verbose for simple cases. `@contextmanager` turns a generator function into a context manager: everything before `yield` is the "enter" logic, the yielded value is the `as` target, and everything after `yield` (typically in a `finally` block) is the "exit" logic. An exception raised inside the `with` block is re-raised at the `yield` point inside the generator, which is exactly why the cleanup code needs to sit in a `finally` (or a `try/except` around the `yield`) to run reliably.

## 5. `contextlib.suppress` — a Built-In for a Common Pattern

```python
from contextlib import suppress

with suppress(FileNotFoundError):
    import os
    os.remove("maybe_missing.txt")
# no exception, no error, whether the file existed or not
```

`suppress(*exception_types)` is a ready-made context manager equivalent to a `try/except: pass` for exactly the listed exception types — useful for "best-effort cleanup" code where a specific, expected failure should be silently ignored, without writing a full try/except for a one-liner.

## 6. Multiple Context Managers in One `with`

```python
with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())
```

Comma-separating context managers in one `with` statement enters them left to right and exits them in **reverse** order (right to left) — equivalent to nesting them, but flatter and more readable. If entering the second one raises, the first one's `__exit__` still runs correctly (Python handles this internally, equivalent to nested `with` blocks under the hood).

## 7. Under the Hood: What `with` Actually Desugars To

```python
with EXPR as name:
    BODY
```

is equivalent to:

```python
mgr = EXPR
aexit = type(mgr).__exit__   # looked up on the TYPE, not the instance
aenter = type(mgr).__enter__
value = aenter(mgr)
exc = True
try:
    try:
        name = value
        BODY
    except:
        exc = False
        if not aexit(mgr, *sys.exc_info()):
            raise
finally:
    if exc:
        aexit(mgr, None, None, None)
```

The key detail most people miss: `__enter__`/`__exit__` are looked up on the object's **type**, not the instance — the same special-method lookup rule that applies to operator overloading (`__add__`, `__len__`, etc.) applies here too, meaning you can't monkey-patch `__exit__` onto a single instance and have `with` see it. This is also exactly why `@contextmanager`-decorated generator functions work: the decorator returns a class (`_GeneratorContextManager`) whose `__enter__`/`__exit__` methods drive the generator via `next()`/`.throw()`, translating the class-based protocol into the generator's `yield`-based one underneath.

![with calls __enter__, runs the body, then always calls __exit__; on an exception, a truthy return from __exit__ swallows it and a falsy one re-raises it](diagrams/01-with-desugaring.svg)

## 8. Common Pitfalls

1. **Forgetting to `return False`/`None` from a custom `__exit__`** — if the last statement happens to evaluate truthy (e.g. `return exc_type is None` when there was no exception evaluates to `True`), it silently suppresses real exceptions raised inside the block; be explicit about the return value.
2. **Writing `@contextmanager` cleanup logic without `try/finally` around the `yield`** — if the block raises, that exception is re-raised at the `yield` point; without `finally`, an exception skips the cleanup code entirely, defeating the purpose of using a context manager at all.
3. **Reusing a single context manager instance across multiple `with` blocks concurrently** — most context managers (including file objects) are not designed to be entered twice at once; each `with` should generally get its own instance unless the class documents reentrancy support.
4. **Assuming `contextlib.suppress` catches subclasses you didn't list** — it uses normal exception matching, so `suppress(ValueError)` does not catch a `TypeError`, even in code where you expected "any input-related error."
5. **Using a bare `try/finally` instead of `with` for resources that already provide a context manager** — reinvents cleanup logic that's already correctly handled (including the exception-info threading through `__exit__`), and is easy to get subtly wrong (e.g. forgetting to close on every exception path).

## 9. Quick Reference

```python
with EXPR as name:                          # __enter__() -> name, guarantees __exit__() on exit
    ...

class C:
    def __enter__(self): return self          # setup, returns the `as` target
    def __exit__(self, exc_type, exc_val, tb):  # ALWAYS called; return True to suppress exception
        return False

from contextlib import contextmanager
@contextmanager
def cm():
    setup()
    try:
        yield value          # the `as` target
    finally:
        teardown()             # always runs

from contextlib import suppress
with suppress(SomeError): ...     # try/except SomeError: pass, as a context manager

with a() as x, b() as y: ...        # enters left-to-right, exits right-to-left
```
