Skip to main content

Python Context Managers: The with Statement and Building Your Own

Advanced25 min6 exercises110 XP
0/6 exercises

Imagine walking through an automatic door at a grocery store. You don't have to push it open or remember to close it behind you. The door opens when you approach and closes when you walk through. It just handles itself.

Python context managers work the same way. They automatically set things up when you enter a block of code and clean things up when you leave. No matter what happens inside — even if an error occurs — the cleanup always runs.

In this tutorial, you'll learn how the with statement works, what happens behind the scenes with __enter__ and __exit__, and how to build your own context managers from scratch.

What Is the with Statement?

The with statement is Python's way of saying: "set something up, let me work with it, and then clean it up automatically." You've probably already seen it when working with files.

Without with (manual cleanup)
f = open('data.txt', 'w')
try:
    f.write('Hello!')
finally:
    f.close()  # You must remember this!
With with (automatic cleanup)
with open('data.txt', 'w') as f:
    f.write('Hello!')
# File is automatically closed here

Both versions do the same thing. But the with version is shorter, cleaner, and impossible to mess up. You can't forget to close the file because Python does it for you.

Let's see a simpler example we can actually run. Python's built-in open() won't work in our browser environment, so we'll build our own context managers to understand the concept.

A minimal context manager
Loading editor...

Notice the order of the output: "Hello" runs first when we enter, then our work happens, then "Goodbye" runs automatically when the block ends.

How Do __enter__ and __exit__ Work?

Every context manager is just an object with two special methods: __enter__ and __exit__. When Python hits a with statement, it calls these methods in order.

Here's what happens step by step:

  • Python calls __enter__() on the object
  • The return value of __enter__() gets assigned to the variable after as
  • The indented block runs
  • Python calls __exit__() — even if an error occurred in step 3
  • Tracing the lifecycle
    Loading editor...

    What Happens When an Error Occurs Inside with?

    The real superpower of context managers is that __exit__ runs even when things go wrong. This guarantees your cleanup code always executes.

    Cleanup runs even with errors
    Loading editor...

    The cleanup message prints before the except catches the error. This is what makes context managers so reliable — they always get a chance to run their teardown logic.

    You can also choose to suppress an error by returning True from __exit__. This tells Python: "I handled this error, don't raise it."

    Suppressing specific errors
    Loading editor...

    How Do You Build a Class-Based Context Manager?

    Let's build something practical: a timer that measures how long a block of code takes to run. This is a common real-world use case.

    A practical timer context manager
    Loading editor...

    Here's another useful pattern — a context manager that temporarily changes a value and restores it when done.

    Temporarily changing a value
    Loading editor...

    How Do You Build a Context Manager with contextlib?

    Writing a full class with __enter__ and __exit__ is fine, but Python gives you a shortcut. The contextlib module lets you write a context manager as a simple generator function.

    A generator-based context manager
    Loading editor...

    The function splits into three parts:

  • Before `yield` — this is your setup (like __enter__)
  • The `yield` value — this is what gets assigned to the as variable
  • After `yield` — this is your cleanup (like __exit__)
  • Safe generator context manager with try/finally
    Loading editor...

    What Are Some Real-World Context Manager Patterns?

    Context managers show up everywhere in professional Python code. Here are patterns you'll encounter in real projects.

    Pattern 1: Nested context managers. You can nest multiple with statements or use a comma to combine them.

    Nested context managers
    Loading editor...

    Pattern 2: Transaction-like behavior. Set up a state, do work, then commit or roll back.

    Transaction pattern
    Loading editor...

    Practice Exercises

    Predict the Output: Basic with
    Predict Output

    What will this code print? Think carefully about the order of __enter__, the block body, and __exit__.

    class Door:
        def __enter__(self):
            print('Door opens')
            return 'welcome'
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print('Door closes')
            return False
    
    with Door() as msg:
        print(msg)
    Loading editor...
    Write a Logging Context Manager
    Write Code

    Create a class called LogBlock that works as a context manager. It takes a label string in __init__.

  • __enter__ should print START: <label> and return self.
  • __exit__ should print END: <label> and return False.
  • Then use it: with LogBlock('test') as lb: and inside print Running....

    Loading editor...
    Fix the Bug: Missing Cleanup
    Fix the Bug

    This context manager is supposed to always print the cleanup message, even when an error occurs inside the block. But the cleanup never runs when there's an error. Fix the @contextmanager function so cleanup always happens.

    The expected output should be:

    Setup done
    Working...
    Cleanup done
    Loading editor...
    Write an Indentation Context Manager
    Write Code

    Create a context manager called indent_block using @contextmanager from contextlib. It should take a level parameter (an integer).

  • Before the block, print 'Entering level <level>'
  • Yield the string ' ' * level (two spaces repeated level times)
  • After the block, print 'Exiting level <level>'
  • Use try/finally for safety.
  • Then demonstrate with:

    with indent_block(1) as pad:
        print(f'{pad}Hello')
        with indent_block(2) as pad2:
            print(f'{pad2}World')
    Loading editor...
    Refactor: Replace try/finally with a Context Manager
    Refactor

    Refactor this code to use a context manager class called ConnectionSimulator instead of try/finally. The output must remain exactly the same.

    Original code:

    connected = False
    try:
        connected = True
        print('Connected: True')
        print('Sending data...')
    finally:
        connected = False
        print('Connected: False')

    Your class should set self.connected in __enter__ and reset it in __exit__. Print the same messages.

    Loading editor...
    Build a Batch Processor Context Manager
    Write Code

    Create a class called BatchProcessor that collects items and processes them when the block ends.

  • __init__: set self.items to an empty list
  • __enter__: print 'Batch started' and return self
  • Add a method add(self, item) that appends the item to self.items
  • __exit__: print 'Processing N items...' where N is the count, then print each item as ' - <item>', then print 'Batch complete'. Return False.
  • Use it to add 'email', 'report', and 'backup'.

    Loading editor...