Timothy was reviewing database code when he noticed something puzzling. "Margaret, I keep seeing this with statement everywhere. with open(file), with lock, with database.transaction(). What's so special about with? Why not just call the functions normally?"
Margaret smiled knowingly. "Welcome to context managers - Python's elegant solution to resource management. That with statement isn't just syntax sugar - it's a guarantee. A guarantee that resources get cleaned up, locks get released, and connections get closed, even if something goes wrong."
"A guarantee?" Timothy looked intrigued. "How can a keyword guarantee cleanup?"
"Let me show you the problem with solves, and then you'll see why it's one of Python's most important features."
The Puzzle: The Cleanup Problem
Timothy showed Margaret some code he'd been debugging:
# The old way - manual cleanup
def process_file_manual():
"""Manual resource management - what could go wrong?"""
file = open('data.txt', 'r')
data = file.read()
process(data)
file.close() # Will this always execute?
# What if process(data) raises an exception?
# The file never closes!
# Resource leak!
"See the problem?" Timothy pointed. "If process(data) raises an exception, file.close() never runs. The file stays open, eating up resources."
"Exactly," Margaret confirmed. "You could wrap it in try/finally, but that's verbose and error-prone. Watch this:"
# The better way - try/finally
def process_file_try_finally():
"""Using try/finally - correct but verbose"""
file = open('data.txt', 'r')
try:
data = file.read()
process(data)
finally:
file.close() # Guaranteed to run
# The best way - context manager
def process_file_with():
"""Using with - clean and guaranteed"""
with open('data.txt', 'r') as file:
data = file.read()
process(data)
# file.close() automatically called!
"Wait," Timothy said, studying the code. "The with statement automatically calls close()? How does it know what to clean up?"
"Perfect question," Margaret said. "The with statement uses a protocol - the context manager protocol. Let me show you what's really happening."
What Are Context Managers?
Margaret pulled up a detailed explanation:
"""
CONTEXT MANAGERS: Objects that manage resources
A context manager is an object that implements two methods:
- __enter__(): Called when entering the 'with' block
- __exit__(exc_type, exc_val, exc_tb): Called when leaving the 'with' block (even on exception!)
Returns True to suppress the exception, or False/None to let it propagate
THE CONTEXT MANAGER PROTOCOL:
with context_manager as variable:
# Do something
# Is equivalent to:
variable = context_manager.__enter__()
try:
# Do something
finally:
context_manager.__exit__(exc_type, exc_val, exc_tb)
CONTEXT MANAGERS GUARANTEE:
- __exit__ ALWAYS runs (even if exception occurs)
- Resources are cleaned up properly
- No resource leaks
- Cleaner, more readable code
"""
def demonstrate_context_manager_basics():
"""Show how context managers work"""
class SimpleContextManager:
"""A basic context manager"""
def __enter__(self):
print(" Entering context - setting up resources")
return "resource"
def __exit__(self, exc_type, exc_val, exc_tb):
print(" Exiting context - cleaning up resources")
print(f" Exception type: {exc_type}")
print(f" Exception value: {exc_val}")
return False # Don't suppress exceptions
print("Using context manager:")
with SimpleContextManager() as resource:
print(f" Inside with block, resource: {resource}")
print("\nWith exception:")
try:
with SimpleContextManager() as resource:
print(" About to raise exception")
raise ValueError("Something went wrong!")
except ValueError as e:
print(f" Caught: {e}")
print("\n✓ __exit__ runs even when exception occurs!")
demonstrate_context_manager_basics()
Output:
Using context manager:
Entering context - setting up resources
Inside with block, resource: resource
Exiting context - cleaning up resources
Exception type: None
Exception value: None
With exception:
Entering context - setting up resources
About to raise exception
Exiting context - cleaning up resources
Exception type: <class 'ValueError'>
Exception value: Something went wrong!
Caught: Something went wrong!
✓ __exit__ runs even when exception occurs!
Timothy watched the output carefully. "__enter__ runs at the start, __exit__ runs at the end, and __exit__ gets information about any exception. So the context manager sees what went wrong and can clean up accordingly."
"Exactly," Margaret confirmed. "Notice how __exit__ received the exception details - exc_type, exc_val, and exc_tb. It can use that information to decide how to clean up, or even suppress the exception by returning True."
"But writing a class with __enter__ and __exit__ for every resource seems verbose," Timothy observed. "Is there a simpler way?"
"There is!" Margaret's eyes lit up. "Python provides a decorator that turns a generator function into a context manager. Let me show you the elegant way."
The @contextmanager Decorator
Margaret opened a new example:
from contextlib import contextmanager
def demonstrate_contextmanager_decorator():
"""Show @contextmanager decorator"""
@contextmanager
def simple_context():
"""Create context manager from generator"""
print(" Setup (before yield)")
yield "resource"
print(" Cleanup (after yield)")
print("Using @contextmanager:")
with simple_context() as resource:
print(f" Inside with block: {resource}")
print("\nWith exception:")
try:
with simple_context() as resource:
print(" About to raise exception")
raise ValueError("Error!")
except ValueError as e:
print(f" Caught: {e}")
print("\n✓ Cleanup still happens after exception!")
demonstrate_contextmanager_decorator()
Output:
Using @contextmanager:
Setup (before yield)
Inside with block: resource
Cleanup (after yield)
With exception:
Setup (before yield)
About to raise exception
Cleanup (after yield)
Caught: Error!
✓ Cleanup still happens after exception!
"That's brilliant!" Timothy exclaimed. "The @contextmanager decorator turns a generator function into a context manager. Everything before yield is the setup (__enter__), everything after is the cleanup (__exit__)."
"Exactly. The generator yields the resource, pauses, waits for the with block to complete, then resumes for cleanup. It's much cleaner than writing a full class."
"So when would I use this in real code?" Timothy asked. "What are the practical patterns?"
Real-World Use Case 1: File Handling
"File handling is the classic example," Margaret said, pulling up a comparison:
import os
import tempfile
def demonstrate_file_handling():
"""Show file context managers"""
# Create a test file
with open('test.txt', 'w') as f:
f.write('Hello, World!')
print("Reading file with context manager:")
with open('test.txt', 'r') as file:
content = file.read()
print(f" Content: {content}")
print(f" File closed? {file.closed}")
print("\nWithout context manager (manual):")
file = open('test.txt', 'r')
content = file.read()
print(f" Content: {content}")
print(f" File closed? {file.closed}")
file.close()
print(f" After manual close: {file.closed}")
# Custom file context manager
@contextmanager
def atomic_write(filename):
"""Write to temp file, then move to target (atomic operation)"""
temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False)
try:
yield temp_file
temp_file.close()
os.replace(temp_file.name, filename) # Atomic on POSIX
except:
temp_file.close()
os.unlink(temp_file.name) # Delete temp file on error
raise
print("\nAtomic write example:")
with atomic_write('atomic.txt') as f:
f.write('This write is atomic!')
print(" ✓ File only appears after successful write!")
# Cleanup
os.unlink('test.txt')
os.unlink('atomic.txt')
demonstrate_file_handling()
Output:
Reading file with context manager:
Content: Hello, World!
File closed? True
Without context manager (manual):
Content: Hello, World!
File closed? False
After manual close: True
Atomic write example:
✓ File only appears after successful write!
"I see the advantage," Timothy noted. "With the context manager, the file is automatically closed even if I forget or if an exception occurs. And the atomic write pattern is elegant - the file only appears if the write succeeds completely."
"Right. File objects are built-in context managers, but you can create custom ones for specific behaviors like atomic writes, compression, or encryption."
"What about thread locks?" Timothy asked. "I've seen with lock: in multithreaded code."
Real-World Use Case 2: Thread Locks
Margaret pulled up threading examples:
import threading
import time
def demonstrate_thread_locks():
"""Show lock context managers"""
counter = 0
lock = threading.Lock()
def unsafe_increment():
"""Without lock - race condition"""
nonlocal counter
temp = counter
time.sleep(0.0001) # Simulate work
counter = temp + 1
def safe_increment():
"""With lock - thread safe"""
nonlocal counter
with lock: # Lock acquired here
temp = counter
time.sleep(0.0001) # Simulate work
counter = temp + 1
# Lock automatically released here!
# Without lock - race condition
counter = 0
threads = [threading.Thread(target=unsafe_increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Without lock: counter = {counter} (expected 10)")
# With lock - safe
counter = 0
threads = [threading.Thread(target=safe_increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"With lock: counter = {counter} (expected 10)")
print("\n✓ Context manager ensures lock is always released!")
demonstrate_thread_locks()
Output:
Without lock: counter = 3 (expected 10)
With lock: counter = 10 (expected 10)
✓ Context manager ensures lock is always released!
"Perfect!" Timothy said. "Without the context manager, if an exception occurred while holding the lock, the lock would never be released. Other threads would deadlock waiting forever."
"Exactly. The with lock: pattern guarantees the lock is released, even if the code inside raises an exception. This prevents deadlocks."
"What about database transactions?" Timothy asked. "I've seen with db.transaction():."
Real-World Use Case 3: Database Transactions
Margaret opened a database example:
def demonstrate_database_transactions():
"""Show transaction context managers"""
class Database:
"""Simulated database with transactions"""
def __init__(self):
self.data = []
self.in_transaction = False
@contextmanager
def transaction(self):
"""Transaction context manager"""
print(" BEGIN TRANSACTION")
self.in_transaction = True
temp_data = self.data.copy()
try:
yield self
print(" COMMIT")
# Transaction successful, changes persist
except Exception as e:
print(f" ROLLBACK (due to: {e})")
self.data = temp_data # Restore original data
raise
finally:
self.in_transaction = False
def insert(self, value):
"""Insert data"""
if self.in_transaction:
self.data.append(value)
print(f" Inserted: {value}")
else:
raise RuntimeError("Must be in transaction")
db = Database()
print("Successful transaction:")
with db.transaction():
db.insert("Alice")
db.insert("Bob")
print(f" Final data: {db.data}\n")
print("Failed transaction:")
try:
with db.transaction():
db.insert("Charlie")
raise ValueError("Something went wrong!")
db.insert("David") # Never executed
except ValueError:
pass
print(f" Final data: {db.data}")
print(" ✓ Charlie was rolled back!")
demonstrate_database_transactions()
Output:
Successful transaction:
BEGIN TRANSACTION
Inserted: Alice
Inserted: Bob
COMMIT
Final data: ['Alice', 'Bob']
Failed transaction:
BEGIN TRANSACTION
Inserted: Charlie
ROLLBACK (due to: Something went wrong!)
Final data: ['Alice', 'Bob']
✓ Charlie was rolled back!
"That's powerful!" Timothy exclaimed. "The transaction automatically commits on success or rolls back on failure. All the transaction logic is in the context manager - the business code stays clean."
"Exactly," Margaret said. "Notice how the except Exception as e block catches any exception, uses it to print the rollback message, restores the original data, then re-raises it with raise. The context manager handles the rollback logic, but the exception still propagates to the caller so they know the transaction failed. This is why frameworks like SQLAlchemy, Django ORM, and others use context managers for transactions. It's the perfect pattern for all-or-nothing operations."
"Can context managers handle more complex setup and cleanup?" Timothy asked.
Advanced Pattern: Suppressing Exceptions
"Absolutely," Margaret said. "Context managers can suppress exceptions by returning True from __exit__. Let me show you:"
def demonstrate_suppressing_exceptions():
"""Show exception suppression in context managers"""
class IgnoreException:
"""Context manager that suppresses specific exceptions"""
def __init__(self, *exceptions):
self.exceptions = exceptions
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
return False
if issubclass(exc_type, self.exceptions):
print(f" Suppressing {exc_type.__name__}: {exc_val}")
return True # Suppress the exception
return False # Don't suppress other exceptions
print("Suppressing ValueError:")
with IgnoreException(ValueError):
print(" Before exception")
raise ValueError("This will be suppressed")
print(" After exception (won't print)")
print(" Continued execution!\n")
print("Not suppressing TypeError:")
try:
with IgnoreException(ValueError):
print(" Before exception")
raise TypeError("This will NOT be suppressed")
print(" After exception (won't print)")
except TypeError as e:
print(f" Caught: {e}")
# Python's built-in suppress
from contextlib import suppress
print("\nUsing contextlib.suppress:")
with suppress(FileNotFoundError):
with open('nonexistent.txt', 'r') as f:
pass
print(" ✓ FileNotFoundError was suppressed!")
demonstrate_suppressing_exceptions()
Output:
Suppressing ValueError:
Before exception
Suppressing ValueError: This will be suppressed
Continued execution!
Not suppressing TypeError:
Before exception
Caught: This will NOT be suppressed
Using contextlib.suppress:
✓ FileNotFoundError was suppressed!
"So __exit__ can inspect the exception and decide whether to suppress it," Timothy observed. "And Python even has a built-in suppress() context manager for common cases."
"Exactly. But be careful - suppressing exceptions should be done thoughtfully. Usually you want to know when errors occur."
"What about reusing context managers?" Timothy asked. "Can I use the same one multiple times?"
Context Manager Reusability
Margaret showed the patterns:
def demonstrate_reusability():
"""Show reusable vs non-reusable context managers"""
class ReusableContext:
"""Can be used multiple times"""
def __enter__(self):
print(" Entering (reusable)")
return self
def __exit__(self, *args):
print(" Exiting (reusable)")
print("Reusable context manager:")
ctx = ReusableContext()
with ctx:
print(" First use")
with ctx:
print(" Second use")
print("\nGenerator-based (@contextmanager):")
@contextmanager
def one_time_context():
"""Can only be used once (generator exhausts)"""
print(" Setup")
yield "resource"
print(" Cleanup")
gen = one_time_context()
with gen:
print(" Using once")
print("\n Trying to reuse:")
try:
with gen:
print(" Using again")
except RuntimeError as e:
print(f" Error: generator already executing")
print("\n ✓ Must create new generator for each use!")
demonstrate_reusability()
Output:
Reusable context manager:
Entering (reusable)
First use
Exiting (reusable)
Entering (reusable)
Second use
Exiting (reusable)
Generator-based (@contextmanager):
Setup
Using once
Cleanup
Trying to reuse:
Error: generator already executing
✓ Must create new generator for each use!
"So class-based context managers can be reused, but @contextmanager decorated functions create single-use generators," Timothy noted.
"Right. Each time you call a @contextmanager function, you get a fresh generator. This is fine for most use cases - just call the function again when you need it."
"What about nested context managers?" Timothy asked. "Can I use multiple with statements?"
Nested Context Managers
Margaret showed the syntax:
def demonstrate_nested_contexts():
"""Show nested context managers"""
@contextmanager
def resource(name):
print(f" Acquiring {name}")
yield name
print(f" Releasing {name}")
print("Traditional nesting:")
with resource("A"):
with resource("B"):
with resource("C"):
print(" Using A, B, C")
print("\nModern syntax (Python 3.1+):")
with resource("A"), resource("B"), resource("C"):
print(" Using A, B, C")
print("\nParenthesized syntax (Python 3.10+):")
with (
resource("A"),
resource("B"),
resource("C")
):
print(" Using A, B, C")
print("\n✓ All styles work identically!")
demonstrate_nested_contexts()
Output:
Traditional nesting:
Acquiring A
Acquiring B
Acquiring C
Using A, B, C
Releasing C
Releasing B
Releasing A
Modern syntax (Python 3.1+):
Acquiring A
Acquiring B
Acquiring C
Using A, B, C
Releasing C
Releasing B
Releasing A
Parenthesized syntax (Python 3.10+):
Acquiring A
Acquiring B
Acquiring C
Using A, B, C
Releasing C
Releasing B
Releasing A
✓ All styles work identically!
"The modern syntax is much cleaner," Timothy said. "And notice the cleanup happens in reverse order - last acquired, first released. Like a stack."
"Exactly. LIFO - Last In, First Out. This ensures proper cleanup when resources depend on each other."
"What about async context managers?" Timothy asked. "For async/await code?"
Async Context Managers
Margaret pulled up async examples:
import asyncio
def demonstrate_async_context_managers():
"""Show async context managers"""
class AsyncResource:
"""Async context manager using __aenter__ and __aexit__"""
async def __aenter__(self):
print(" Async entering...")
await asyncio.sleep(0.1) # Simulate async setup
print(" Async setup complete")
return self
async def __aexit__(self, *args):
print(" Async exiting...")
await asyncio.sleep(0.1) # Simulate async cleanup
print(" Async cleanup complete")
from contextlib import asynccontextmanager
@asynccontextmanager
async def async_resource(name):
"""Async context manager using decorator"""
print(f" Setting up {name}...")
await asyncio.sleep(0.1)
yield name
print(f" Cleaning up {name}...")
await asyncio.sleep(0.1)
async def main():
print("Using async context manager (class):")
async with AsyncResource():
print(" Inside async with block")
print("\nUsing async context manager (decorator):")
async with async_resource("database"):
print(" Inside async with block")
print("Running async examples:")
asyncio.run(main())
print("✓ Async context managers support async/await!")
demonstrate_async_context_managers()
Output:
Running async examples:
Using async context manager (class):
Async entering...
Async setup complete
Inside async with block
Async exiting...
Async cleanup complete
Using async context manager (decorator):
Setting up database...
Inside async with block
Cleaning up database...
✓ Async context managers support async/await!
"So async context managers use __aenter__ and __aexit__ instead of __enter__ and __exit__, and you use async with instead of with," Timothy summarized.
"Exactly. This lets you await async operations during setup and cleanup. Essential for async database connections, network sockets, and other async resources."
"When shouldn't I use context managers?" Timothy asked.
When NOT to Use Context Managers
Margaret pulled up a guidelines list:
"""
WHEN NOT TO USE CONTEXT MANAGERS:
❌ When there's no cleanup needed
- Pure computations
- Immutable operations
- No resources to manage
❌ When lifetime extends beyond scope
- Objects that persist
- Caches that live for application lifetime
- Background services
❌ When you need manual control
- Conditional cleanup
- Delayed cleanup
- Complex state machines
❌ Over-engineering simple code
- Don't create context managers for everything
- Sometimes a simple function is clearer
GOOD USE CASES:
✓ File I/O
✓ Locks and synchronization
✓ Database transactions
✓ Network connections
✓ Temporary state changes
✓ Resource pools
✓ Timing/profiling
✓ Error context
"""
def demonstrate_when_not_to_use():
"""Show cases where context managers are overkill"""
# ❌ BAD: No cleanup needed
@contextmanager
def pointless_context():
yield # Does nothing useful
# ✓ GOOD: Just use a function
def calculate_sum(numbers):
return sum(numbers)
# ❌ BAD: Lifetime extends beyond scope
@contextmanager
def create_cache():
cache = {}
yield cache
# Cache should persist, not be cleaned up!
# ✓ GOOD: Just create and return
def create_cache_properly():
return {}
print("Don't over-engineer with context managers!")
print(" Use them when you need guaranteed cleanup")
print(" Not for every resource or function")
demonstrate_when_not_to_use()
"So context managers are for resources that need cleanup," Timothy said. "If there's no cleanup, a regular function is simpler."
"Exactly. Context managers are powerful, but don't force the pattern where it doesn't fit."
Common Pitfalls
Margaret showed common mistakes:
def demonstrate_pitfalls():
"""Show common context manager pitfalls"""
print("Pitfall 1: Forgetting to handle exceptions in @contextmanager")
@contextmanager
def fragile_context():
print(" Setup")
resource = acquire_resource()
yield resource
# If yield raises, cleanup never happens!
print(" Cleanup")
release_resource(resource)
# ✓ BETTER: Use try/finally
@contextmanager
def robust_context():
print(" Setup")
resource = acquire_resource()
try:
yield resource
finally:
print(" Cleanup (guaranteed)")
release_resource(resource)
print("\nPitfall 2: Yielding multiple times")
try:
@contextmanager
def multi_yield():
yield 1
yield 2 # Error! Can only yield once
with multi_yield() as x:
pass
except RuntimeError as e:
print(f" Error: {e}")
print("\nPitfall 3: Not returning anything from __enter__")
class NoReturn:
def __enter__(self):
pass # Returns None
def __exit__(self, *args):
pass
with NoReturn() as x:
print(f" x is: {x}") # None!
print("\n ✓ Always return self or resource from __enter__!")
def acquire_resource():
return "resource"
def release_resource(resource):
pass
demonstrate_pitfalls()
"Good to know the gotchas," Timothy said. "Always use try/finally in @contextmanager, only yield once, and return something useful from __enter__."
Testing Context Managers
Margaret showed testing patterns:
import pytest
from unittest.mock import Mock, patch
def demonstrate_testing():
"""Show how to test context managers"""
@contextmanager
def file_writer(filename):
"""Context manager to test"""
f = open(filename, 'w')
try:
yield f
finally:
f.close()
def test_context_manager_success():
"""Test normal operation"""
with file_writer('test.txt') as f:
f.write('test')
with open('test.txt') as f:
assert f.read() == 'test'
def test_context_manager_exception():
"""Test cleanup happens on exception"""
try:
with file_writer('test2.txt') as f:
f.write('test')
raise ValueError("Error!")
except ValueError:
pass
# The important part: exception was raised and handled
# File cleanup (close) still happened due to finally block
print(" Exception handled, cleanup executed")
def test_with_mock():
"""Test using mocks"""
mock_enter = Mock(return_value='resource')
mock_exit = Mock(return_value=False)
mock_cm = Mock()
mock_cm.__enter__ = mock_enter
mock_cm.__exit__ = mock_exit
with mock_cm as resource:
assert resource == 'resource'
mock_enter.assert_called_once()
mock_exit.assert_called_once()
print("Testing context managers:")
test_context_manager_success()
print(" ✓ Success case tested")
test_context_manager_exception()
print(" ✓ Exception case tested")
test_with_mock()
print(" ✓ Mock testing works")
import os
if os.path.exists('test.txt'):
os.unlink('test.txt')
demonstrate_testing()
The Door Metaphor
Margaret brought it back to a metaphor:
"Think of context managers like automatic doors," she said.
"When you approach an automatic door (with), it opens for you (__enter__). You walk through and do your business. When you leave, the door automatically closes behind you (__exit__), no matter what happened inside.
"Even if you:
- Trip and fall (exception)
- Run through quickly (early return)
- Take your time (normal execution)
The door always closes. You never have to worry about leaving it open.
"Manual resource management is like a regular door - you have to remember to close it. Miss once, and you've got a problem. Context managers are automatic doors - they close themselves, guaranteed."
Key Takeaways
Margaret summarized:
"""
CONTEXT MANAGER KEY TAKEAWAYS:
1. What are context managers:
- Objects that implement __enter__ and __exit__
- Guarantee cleanup even on exceptions
- Used with 'with' statement
- Essential for resource management
2. The protocol:
- __enter__(): Setup, return resource
- __exit__(exc_type, exc_val, exc_tb): Cleanup
- __exit__ ALWAYS runs (even on exception)
- Return True from __exit__ to suppress exception
3. Two ways to create:
- Class with __enter__ and __exit__
- @contextmanager decorator with generator
4. @contextmanager pattern:
@contextmanager
def resource():
# Setup (before yield)
resource = acquire()
try:
yield resource
finally:
# Cleanup (after yield)
release(resource)
5. Real-world uses:
- File I/O (open, close)
- Locks (acquire, release)
- Database transactions (begin, commit/rollback)
- Network connections (open, close)
- Temporary state changes
- Timing/profiling
6. Built-in context managers:
- open() for files
- threading.Lock()
- contextlib.suppress()
- tempfile.TemporaryDirectory()
- decimal.localcontext()
7. Advanced features:
- Nested contexts (comma syntax)
- Async context managers (__aenter__, __aexit__)
- Exception suppression (return True)
- Reusable contexts (class-based)
8. When to use:
- Need guaranteed cleanup
- Resource management
- Temporary state changes
- Transaction-like operations
- Setup/teardown pairs
9. When NOT to use:
- No cleanup needed
- Lifetime extends beyond scope
- Need manual control
- Over-engineering simple code
10. Common pitfalls:
- Forgetting try/finally in @contextmanager
- Yielding multiple times
- Not returning resource from __enter__
- Reusing generator-based contexts
- Suppressing exceptions unintentionally
"""
Timothy nodded, understanding crystallizing. "So context managers are Python's way of guaranteeing cleanup. The with statement ensures __exit__ always runs, even if exceptions occur. I can create them with classes or with the @contextmanager decorator. They're perfect for any resource that needs setup and teardown - files, locks, transactions, connections. It's like having automatic doors instead of manual ones - they always close behind you."
"Perfect understanding," Margaret confirmed. "Context managers are one of Python's most important features. They make resource management safe, predictable, and readable. Once you start using them, you'll see opportunities everywhere - not just files and locks, but timing code, changing settings temporarily, managing state, and countless other patterns. The with statement is your guarantee that cleanup happens, no matter what."
With that knowledge, Timothy could write safe resource management code, understand how frameworks use context managers for transactions and connections, create his own context managers for custom patterns, and avoid resource leaks that plague code without proper cleanup.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)