šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python Context Managers

Master the with statement by implementing __enter__ and __exit__ directly, then simplify with contextlib.contextmanager — the idiomatic way to guarantee cleanup runs.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

Is __exit__ guaranteed to run even if the code inside the with block raises an exception?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

with open(...) as f: is the most common context manager most Python developers ever write, but rarely the last one they need. This lesson covers the __enter__/__exit__ protocol behind with, and contextlib.contextmanager, the generator-based shortcut for writing your own.

1The __enter__/__exit__ Contract Behind with

with EXPR as VAR: is defined, precisely, as: evaluate EXPR to get a context manager object, call its __enter__() method and bind the result to VAR, run the indented block, and then — no matter how that block ends, whether it completes normally, returns, breaks, or raises an exception — call the context manager's __exit__() method exactly once before control leaves the with statement.

That 'no matter how it ends' guarantee is the entire reason with exists: it's structurally identical to writing a try/finally, except the cleanup logic lives inside the reusable context manager object instead of being retyped at every call site. open() returns a file object that implements this protocol, which is why with open(path) as f: guarantees f.close() runs even if the code reading f raises partway through — a plain f = open(path) without with offers no such guarantee.

__enter__'s return value becomes the as variable — often, but not necessarily, self; some context managers (like certain database transaction wrappers) return a different, more specific object than the context manager itself, one specifically meant for use inside the block.

āœ•
—
+
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self  # becomes the 'as' variable

    def __exit__(self, exc_type, exc_value, traceback):
        import time
        print(f"Elapsed: {time.time() - self.start:.4f}s")
        return False  # False = don't suppress the exception

with Timer() as t:
    print("doing work...")
localhost:3000
Console Output
with Timer() as t: ...
Elapsed: 0.0012s — printed even if the block raises

2__exit__'s Three Arguments and Exception Suppression

__exit__(self, exc_type, exc_value, traceback) receives full information about any exception that occurred inside the with block: exc_type is the exception class (or None if the block completed without error), exc_value is the actual exception instance, and traceback is its traceback object. This is what lets a context manager behave differently depending on whether — and how — the block failed, not just run identical cleanup regardless.

The return value of __exit__ has special, specific meaning: if it evaluates to True, Python treats the exception as *handled* and does not propagate it any further — execution resumes normally after the with block, as if nothing had gone wrong. If __exit__ returns False (or None, which is what happens if you don't explicitly return anything, since Python functions default to returning None), the exception continues propagating exactly as if the with statement weren't there.

This is a sharp, easy-to-misuse tool: swallowing exceptions silently is often a bug waiting to happen, hiding real failures from callers who have no idea something went wrong. The idiomatic use is narrow and deliberate — check exc_type for a *specific* exception class you genuinely intend to suppress (as in SuppressValueError), and always return False for anything else so unrelated errors still propagate normally.

āœ•
—
+
class SuppressValueError:
    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   # exception is swallowed here
        return False       # anything else propagates normally

with SuppressValueError():
    raise ValueError("this never escapes the with block")
print("execution continues normally")
localhost:3000
Exception Handling
with SuppressValueError(): raise ValueError(...)
Suppressed — execution continues past the with block

3@contextmanager: A Generator Shortcut for Simple Cases

Writing a full class with __enter__ and __exit__ for a simple 'do setup, run the block, do cleanup' context manager is more ceremony than the logic usually deserves. contextlib.contextmanager lets you write the same behavior as a generator function with exactly one yield: everything before yield becomes __enter__'s logic, the value passed to yield becomes the as variable, and everything after yield — critically, wrapped in a try/finally — becomes __exit__'s cleanup logic.

The finally is not optional ceremony; it's what makes the generator-based context manager honor the same 'cleanup always runs' guarantee as the class-based version. Without it, an exception raised inside the with block would propagate straight out of the generator at the yield line, skipping whatever cleanup code comes after — silently breaking the exact contract with exists to provide.

@contextmanager is idiomatic and heavily used throughout the standard library and popular frameworks (contextlib.suppress, database session helpers, temporary environment variable overrides) specifically because most context managers really are this simple: acquire something, yield control, release it — and the generator syntax expresses that shape more directly than a class with two separate methods that have to agree on shared state via self.

āœ•
—
+
from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.time()
    try:
        yield  # code inside the 'with' block runs here
    finally:
        print(f"Elapsed: {time.time() - start:.4f}s")

with timer():
    print("doing work...")
localhost:3000
Generator-Based Cleanup
@contextmanager def timer():
yield splits setup from guaranteed (try/finally) teardown

4Step-by-Step Breakdown

with guarantees a file gets closed even if the code inside raises an exception. That guarantee comes from a two-method protocol — let's implement it ourselves.

with calls __enter__ at the start of the block and __exit__ at the end — guaranteed, even if an exception is raised inside the block.

Checkpoint: Is __exit__ guaranteed to run even if the code inside the with block raises an exception?

  • →Yes — __exit__ always runs, exception or not, similar to a finally block
  • →No — an exception skips __exit__ entirely

__exit__'s three parameters describe any exception that occurred. Returning True from __exit__ suppresses that exception entirely.

Checkpoint: What does returning True from __exit__ do to an exception that occurred inside the with block?

  • →It suppresses the exception — code after the with block continues normally
  • →It re-raises the exception with additional context

contextlib.contextmanager turns a generator with exactly one yield into a context manager — no class needed for simple cases.

You've now covered functions-that-wrap-functions, functions-that-pause, and objects-that-hook-into-language-statements — descriptors take that last idea even further, letting an object control what happens on plain attribute access.

Implement Real Enter/Exit Hooks. Finish Timer.__exit__(): __exit__ always runs when the with block ends.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Wrap the yield in try/finally when writing a @contextmanager generator

Without it, an exception inside the with block skips your cleanup code entirely, silently breaking the guarantee that with exists to provide.

Only suppress specific, anticipated exception types from __exit__

Returning True unconditionally from __exit__ hides every error, including genuine bugs unrelated to what the context manager was designed to handle. Check exc_type against a specific class before suppressing.

Frequent Bugs

THE BUG

Writing a @contextmanager generator without a try/finally around yield, so an exception inside the with block causes the generator's cleanup code (after yield) to never execute.

THE FIX

Always structure a @contextmanager generator as: setup code, try: yield value, finally: cleanup code — the finally guarantees cleanup runs regardless of what happens inside the with block.

Real-World Examples

A Temporary Working Directory Context Manager

A test suite needs to temporarily change the current working directory for the duration of a test, then reliably restore the original directory afterward, even if the test fails.

from contextlib import contextmanager
import os

@contextmanager
def temporary_cwd(path: str):
    original = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(original)

with temporary_cwd("/tmp/test_fixtures"):
    run_test_that_reads_relative_files()

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Writing a @contextmanager generator without try/finally, so cleanup code after yield is silently skipped whenever the with block raises an exception.

# Wrong: cleanup skipped if the with block raises @contextmanager def timer(): start = time.time() yield print(f"Elapsed: {time.time() - start:.4f}s") # never runs on exception # Correct @contextmanager def timer(): start = time.time() try: yield finally: print(f"Elapsed: {time.time() - start:.4f}s")

The Solution //

Wrap the yield in try/finally so cleanup runs unconditionally, matching the guarantee a class-based __exit__ provides automatically.

Lesson Glossary

[01]Context manager

An object implementing __enter__ and __exit__, usable in a with statement to guarantee setup and cleanup logic runs.

Code Preview
// Context manager context

[02]__enter__

The context manager method called at the start of a with block; its return value is bound to the as variable.

Code Preview
// __enter__ context

[03]__exit__

The context manager method called when a with block ends (normally or via exception), responsible for cleanup and optional exception suppression.

Code Preview
// __exit__ context

[04]@contextmanager

A contextlib decorator that converts a single-yield generator function into a full context manager.

Code Preview
// @contextmanager context

Continue Learning