-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
319: Using ExitStack for Dynamic Resource Management
A few years ago, I was reviewing a PR from a junior dev who was building a tool to synchronize data across a variable number of remote servers. He had a list of connection strings that changed based on a config file. His first instinct was to use a for loop to open the connections, but he quickly realized that if the third connection failed, the first two remained open, leaking resources until the process finally died. He tried to fix it by nesting with statements, but since he didn't know if there would be two servers or twenty, he couldn't hardcode the blocks. He ended up with a mess of try...finally blocks that was almost impossible to read. He was essentially trying to manually recreate a context manager's logic, which is a path to madness.
This is exactly where contextlib.ExitStack comes in. You already know how with blocks work for single resources, but ExitStack is the tool you use when the number of resources you need to manage is dynamic or depends on runtime data. Think of it as a "bucket" for context managers. You can keep throwing resources into the bucket, and when the bucket itself is closed, it ensures every single resource inside is shut down in the reverse order they were added.
Escaping the Indentation Pyramid
When you're dealing with a fixed number of resources, nesting with open(a) as a, open(b) as b: is fine. But if you have ten files, your code starts drifting off the right side of your screen. Even worse, if you try to put a with statement inside a loop, you aren't actually keeping all those files open; you're opening and closing them one by one. If your logic requires all resources to be open simultaneously—say, you're reading from five different logs to find a correlating timestamp—a standard loop won't cut it.
By using ExitStack, you can maintain a flat indentation level regardless of how many resources you're managing. You call stack.enter_context() for each resource, and Python handles the cleanup logic behind the scenes. If an exception occurs while opening the fifth file, ExitStack is smart enough to close the four files that were already successfully opened before propagating the error.
Managing Dynamic Resources at Runtime
Let's look at how this actually looks in code. Imagine you have a list of file paths provided by a user, and you need to read a line from each of them simultaneously to process a data row.
from contextlib import ExitStack
def process_multiple_files(filepaths):
with ExitStack() as stack:
# We use a list comprehension to open all files
# enter_context returns the object returned by the __enter__ method
files = [stack.enter_context(open(p, 'r')) for p in filepaths]
# Now we have a list of open file handles
# We can iterate through them as much as we want
for line_set in zip(*(f for f in files)):
print(f"Processing row: {line_set}")
# All files are guaranteed to be closed here,
# regardless of whether the loop finished or crashed.
I love this pattern because it separates the acquisition of resources from the logic of the program. You aren't worrying about f.close() calls in a finally block. The ExitStack tracks everything. It’s essentially a stack (LIFO), meaning the last resource you entered is the first one to be closed, which is crucial when you have dependent resources (like a database connection that must stay open while a cursor is active).
📋 Practical Task
Exercise: Building a Multi-Source Log Merger
You are tasked with creating a function called merge_logs(log_files). This function should take a list of filenames as input. It must open all these files simultaneously using an ExitStack, read the first line from each file, and return them as a combined string separated by a pipe character (|).
Requirements:
- Use
contextlib.ExitStackto manage the file handles. - Ensure that all files are closed properly, even if one of the files in the list does not exist (which would trigger an
FileNotFoundError). - Do not use a fixed number of
withstatements; the solution must work for any number of files passed in the list.
# Template to get you started:
from contextlib import ExitStack
def merge_logs(log_files):
# Your code here
pass
# Test your code with:
# with open('test1.log', 'w') as f: f.write('Error A')
# with open('test2.log', 'w') as f: f.write('Error B')
# print(merge_logs(['test1.log', 'test2.log'])) # Expected: "Error A|Error B"
There are no comments for now.