Ads and ad measurement. Disabled until you allow it.
ABOUT SITE
All open site tabs
Python · Syntax · Advanced
Context managers
10 tasks
Objects that manage setup and teardown with the `with` statement. Covers `__enter__`/`__exit__`, `contextlib.contextmanager`, and resource management patterns.
Quick topic start and explanations before exercises (exercises below):
__enter__, __exit__, and exception handling in depth
**Why context managers exist**
Resource management follows a strict pattern: *acquire → use → release*. The release step must happen even if an exception occurs during 'use'. Without a guaranteed cleanup, bugs leave files open, database connections leaking, and locks never released.
The naive approach with `try/finally` works but forces every caller to repeat the cleanup logic:
```python
# Repetitive — every caller must write this boilerplate
f = open('data.txt')
try:
content = f.read()
finally:
f.close() # must close even if f.read() raised
```
A context manager encapsulates both the setup and teardown in one reusable object, and the `with` statement calls them automatically:
```python
with open('data.txt') as f:
content = f.read()
# file is closed here, even if f.read() raised
```
**The protocol: `__enter__` and `__exit__`**
Any object with these two methods can be used in a `with` statement:
```python
class Timer:
def __enter__(self):
import time
self._start = time.perf_counter()
return self # this becomes the 'as' target
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.perf_counter() - self._start
print(f'elapsed: {elapsed:.4f}s')
return False # False = do not suppress exceptions
with Timer() as t:
sum(range(1_000_000))
# elapsed: 0.0312s (printed by __exit__)
```
**What the three `__exit__` arguments mean**
`__exit__(self, exc_type, exc_val, exc_tb)` is called when the `with` block ends — whether normally or due to an exception.
- If the block ended *without* an exception: all three are `None`
- If an exception *was* raised: `exc_type` is the exception class, `exc_val` is the instance, `exc_tb` is the traceback
The return value controls whether the exception propagates:
```python
class SuppressKeyError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is KeyError:
print(f'Suppressed missing key: {exc_val}')
return True # True = swallow the exception
return False # False = let it propagate
d = {'a': 1}
with SuppressKeyError():
print(d['missing']) # Suppressed missing key: 'missing'
print('continues here') # execution resumes normally
```
Returning `True` from `__exit__` is how you suppress exceptions. Returning `False` (or `None`, which is falsy) lets them propagate. Only suppress exceptions deliberately — accidentally returning `True` hides bugs.
**`@contextmanager` — write a context manager as a generator**
Writing a full class for simple context managers is verbose. `contextlib.contextmanager` lets you write one as a generator function with a single `yield`:
```python
from contextlib import contextmanager
@contextmanager
def managed_connection(host):
conn = connect(host) # __enter__: setup
try:
yield conn # 'as' target; code inside 'with' runs here
finally:
conn.close() # __exit__: teardown — always runs
with managed_connection('localhost') as conn:
conn.query('SELECT 1')
```
The `try/finally` around `yield` is essential. Without it, an exception inside the `with` block would skip `conn.close()` — defeating the whole purpose:
```python
@contextmanager
def bad_manager(): # BUG: missing try/finally
resource = acquire()
yield resource
release(resource) # never reached if body raises!
@contextmanager
def good_manager(): # correct
resource = acquire()
try:
yield resource
finally:
release(resource) # always runs
```
**`contextlib.suppress(*exc_types)` — inline exception suppression**
Equivalent to `try/except` that catches and ignores specific exceptions:
```python
from contextlib import suppress
import os
# Without suppress:
try:
os.remove('maybe_exists.txt')
except FileNotFoundError:
pass
# With suppress:
with suppress(FileNotFoundError):
os.remove('maybe_exists.txt')
```
**`contextlib.ExitStack` — dynamic context manager composition**
`ExitStack` manages a variable number of context managers that you don't know at write time. It also works as an escape hatch when something goes wrong during setup:
```python
from contextlib import ExitStack
# Open a variable-length list of files safely:
filenames = ['a.txt', 'b.txt', 'c.txt']
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in filenames]
# All files are open here
for f in files:
print(f.read())
# All files are closed here — even if one read() raised
# ExitStack as a cleanup-on-failure pattern:
def setup_resources():
stack = ExitStack()
try:
conn = stack.enter_context(get_connection())
lock = stack.enter_context(get_lock())
return conn, lock, stack # caller owns the stack
except Exception:
stack.close() # clean up what was acquired so far
raise
```
**Nesting context managers**
You can open multiple context managers in one `with` statement — they're entered left to right and exited right to left (like a stack):
```python
# Both styles are identical:
with open('in.txt') as src, open('out.txt', 'w') as dst:
dst.write(src.read())
# Equivalent nested form:
with open('in.txt') as src:
with open('out.txt', 'w') as dst:
dst.write(src.read())
```
class vs @contextmanager, real patterns, nullcontext, common mistakes
**class vs `@contextmanager` — when to choose which**
Both produce a context manager. The choice comes down to complexity and reuse:
| Situation | Use |
|---|---|
| Simple setup + teardown, one-off | `@contextmanager` |
| Need to customise `__enter__` return value in complex ways | Class |
| Want to subclass or add methods | Class |
| Sharing the CM across multiple threads or re-entering it | Class (be careful with state) |
| Testing — you want to mock or subclass it | Class |
**Real-world patterns**
```python
from contextlib import contextmanager
# 1. Temporary directory (stdlib already has this, but illustrative)
@contextmanager
def temp_dir():
import tempfile, shutil
d = tempfile.mkdtemp()
try:
yield d
finally:
shutil.rmtree(d)
# 2. Temporary attribute override
@contextmanager
def set_attr(obj, name, value):
old = getattr(obj, name, None)
setattr(obj, name, value)
try:
yield
finally:
if old is None:
delattr(obj, name)
else:
setattr(obj, name, old)
# 3. Indented text writer
@contextmanager
def indent(writer, spaces=4):
writer.depth = getattr(writer, 'depth', 0) + spaces
try:
yield writer
finally:
writer.depth -= spaces
```
**`contextlib.nullcontext` — a no-op context manager**
When you sometimes want a context manager and sometimes don't, `nullcontext` acts as a transparent placeholder:
```python
from contextlib import nullcontext
def process(data, lock=None):
cm = lock if lock is not None else nullcontext()
with cm:
return expensive_computation(data)
process(data, lock=threading.Lock()) # thread-safe
process(data) # no locking — same code path
```
**Common mistakes**
Missing `try/finally` in `@contextmanager` — the single most common bug. The teardown code after `yield` is skipped if the `with` body raises.
Returning a value from `__exit__` by accident — any truthy value suppresses the exception. Forgetting `return False` at the bottom of `__exit__` is safe (Python treats `None` as falsy), but `return exc_tb` or `return 1` will silently swallow exceptions.
Using a context manager after `with` exits — the resource is closed/released; the `as` variable still points to the object but it's in a closed state. Always complete all resource usage *inside* the `with` block.
Write a class-based context manager `Timer` that measures elapsed time. `__enter__` should record the start time and return `self`. `__exit__` should compute and store the elapsed time in `self.elapsed`. After the `with` block, `timer.elapsed` should hold the elapsed seconds.
import time
class Timer:
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
with Timer() as t:
total = sum(range(1_000_000))
print(f'Elapsed: {t.elapsed:.4f}s')
Solution
import time
class Timer:
def __enter__(self):
self._start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.time() - self._start
return False # do not suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
print(f'Elapsed: {t.elapsed:.4f}s')
Write a context manager `ManagedFile(path, mode)` using a class that opens a file in `__enter__` and closes it in `__exit__`. If an exception occurs inside the `with` block, the file should still be closed. Return the file object from `__enter__`.
class ManagedFile:
def __init__(self, path, mode='r'):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
with ManagedFile('/tmp/test.txt', 'w') as f:
f.write('hello context manager\n')
with ManagedFile('/tmp/test.txt', 'r') as f:
print(f.read()) # hello context manager
Solution
class ManagedFile:
def __init__(self, path, mode='r'):
self.path = path
self.mode = mode
self._file = None
def __enter__(self):
self._file = open(self.path, self.mode)
return self._file
def __exit__(self, exc_type, exc_val, exc_tb):
if self._file:
self._file.close()
return False
with ManagedFile('/tmp/test.txt', 'w') as f:
f.write('hello context manager\n')
with ManagedFile('/tmp/test.txt', 'r') as f:
print(f.read())
Rewrite the `Timer` context manager from exercise 1 using `@contextlib.contextmanager` and a generator function instead of a class. The function should `yield` a dict `{'elapsed': None}` and update it with the elapsed time after the `yield`.
import time
from contextlib import contextmanager
@contextmanager
def timer():
pass
with timer() as t:
total = sum(range(1_000_000))
print(f"Elapsed: {t['elapsed']:.4f}s")
Solution
import time
from contextlib import contextmanager
@contextmanager
def timer():
info = {'elapsed': None}
start = time.time()
try:
yield info # 'as' target receives this
finally:
info['elapsed'] = time.time() - start
with timer() as t:
total = sum(range(1_000_000))
print(f"Elapsed: {t['elapsed']:.4f}s")
Write a context manager `suppress_and_log(*exception_types)` that suppresses any of the given exception types and prints `'Suppressed: <exception message>'`. Other exceptions should propagate normally.
from contextlib import contextmanager
@contextmanager
def suppress_and_log(*exception_types):
pass
with suppress_and_log(ValueError, ZeroDivisionError):
result = 1 / 0
print('this line is not reached')
print('execution continues after the with block')
Solution
from contextlib import contextmanager
@contextmanager
def suppress_and_log(*exception_types):
try:
yield
except exception_types as e:
print(f'Suppressed: {e}')
with suppress_and_log(ValueError, ZeroDivisionError):
result = 1 / 0
print('this line is not reached')
print('execution continues after the with block')
# Suppressed: division by zero
# execution continues after the with block
Write a context manager `temp_file(suffix='.txt')` using `@contextmanager` that creates a temporary file, yields its path, and deletes the file when the `with` block exits (even if an exception occurs). Use `tempfile.mktemp()` to generate the path.
import os
import tempfile
from contextlib import contextmanager
@contextmanager
def temp_file(suffix='.txt'):
pass
with temp_file() as path:
with open(path, 'w') as f:
f.write('temporary data')
print(os.path.exists(path)) # True
print(os.path.exists(path)) # False — file deleted
Solution
import os
import tempfile
from contextlib import contextmanager
@contextmanager
def temp_file(suffix='.txt'):
path = tempfile.mktemp(suffix=suffix)
try:
yield path
finally:
if os.path.exists(path):
os.remove(path)
with temp_file() as path:
with open(path, 'w') as f:
f.write('temporary data')
print(os.path.exists(path)) # True
print(os.path.exists(path)) # False
Write a context manager `indented(level=1, indent=' ')` that makes all `print()` calls inside the `with` block print with extra indentation. Patch the built-in `print` function temporarily using `builtins.print`. Restore the original `print` after the block exits.
import builtins
from contextlib import contextmanager
@contextmanager
def indented(level=1, indent=' '):
pass
print('top level')
with indented(2):
print('indented by 4 spaces')
print('also indented')
print('back to top level')
Solution
import builtins
from contextlib import contextmanager
@contextmanager
def indented(level=1, indent=' '):
prefix = indent * level
original_print = builtins.print
def indented_print(*args, **kwargs):
original_print(prefix, end='')
original_print(*args, **kwargs)
builtins.print = indented_print
try:
yield
finally:
builtins.print = original_print
print('top level')
with indented(2):
print('indented by 4 spaces')
print('also indented')
print('back to top level')
Write a class `FakeDB` with a list `log` of committed operations. Implement a context manager `transaction(db)` using `@contextmanager`: it should collect operations in a temporary list, and on clean exit commit them all to `db.log`. On exception, roll back (discard the temp list) and re-raise the exception.
from contextlib import contextmanager
class FakeDB:
def __init__(self):
self.log = []
@contextmanager
def transaction(db):
pass
db = FakeDB()
with transaction(db) as tx:
tx.append('INSERT user')
tx.append('UPDATE balance')
print(db.log) # ['INSERT user', 'UPDATE balance']
try:
with transaction(db) as tx:
tx.append('DELETE everything')
raise RuntimeError('oops')
except RuntimeError:
pass
print(db.log) # still ['INSERT user', 'UPDATE balance'] — rolled back
Solution
from contextlib import contextmanager
class FakeDB:
def __init__(self):
self.log = []
@contextmanager
def transaction(db):
pending = []
try:
yield pending
db.log.extend(pending) # commit on success
except Exception:
pass # rollback — discard pending
raise
db = FakeDB()
with transaction(db) as tx:
tx.append('INSERT user')
tx.append('UPDATE balance')
print(db.log) # ['INSERT user', 'UPDATE balance']
try:
with transaction(db) as tx:
tx.append('DELETE everything')
raise RuntimeError('oops')
except RuntimeError:
pass
print(db.log) # ['INSERT user', 'UPDATE balance']
Use `contextlib.ExitStack` to open a dynamic number of files at once and read their first line. Given a list of file paths, open all of them inside a single `with ExitStack()` block and collect the first line from each file into a list.
import contextlib
def read_first_lines(paths):
pass
import tempfile, os
# Create test files
paths = []
for i in range(3):
p = tempfile.mktemp()
with open(p, 'w') as f:
f.write(f'line from file {i}\nmore lines')
paths.append(p)
print(read_first_lines(paths))
# ['line from file 0', 'line from file 1', 'line from file 2']
for p in paths:
os.remove(p)
Solution
import contextlib
def read_first_lines(paths):
with contextlib.ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
return [f.readline().strip() for f in files]
import tempfile, os
paths = []
for i in range(3):
p = tempfile.mktemp()
with open(p, 'w') as f:
f.write(f'line from file {i}\nmore lines')
paths.append(p)
print(read_first_lines(paths))
for p in paths:
os.remove(p)
Write a class `Attempt` that suppresses `ValueError` exceptions raised inside the `with` block and stores the exception in `self.error`. If no exception occurred, `self.error` should be `None`. Other exception types should propagate normally.
class Attempt:
def __init__(self):
self.error = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
with Attempt() as a:
int('not a number')
print(a.error) # invalid literal for int() with base 10: 'not a number'
with Attempt() as a:
x = int('42')
print(a.error) # None
Solution
class Attempt:
def __init__(self):
self.error = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
self.error = exc_val
return True # suppress the exception
return False # propagate other exceptions
with Attempt() as a:
int('not a number')
print(a.error)
with Attempt() as a:
x = int('42')
print(a.error) # None
Write a `SafeCounter` class with an internal `threading.Lock`. Implement an `increment()` method that uses `with self.lock:` to safely increment `self.count`. Then run 5 threads, each calling `increment()` 1000 times, and verify the final count is exactly 5000.
import threading
class SafeCounter:
def __init__(self):
self.count = 0
self.lock = threading.Lock()
def increment(self):
pass
counter = SafeCounter()
threads = [threading.Thread(target=lambda: [counter.increment() for _ in range(1000)])
for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(counter.count) # 5000
Solution
import threading
class SafeCounter:
def __init__(self):
self.count = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
self.count += 1
counter = SafeCounter()
threads = [threading.Thread(target=lambda: [counter.increment() for _ in range(1000)])
for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(counter.count) # 5000
No split tab
Cookie preferences
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.