# Python by Structure: Context Managers and the With Statement

Timothy stared at his screen in frustration. His script had crashed halfway through processing a batch of log files, and now he couldn't open any of them. "Margaret, I keep getting 'too many open files' errors. But I'm closing them! Look - I have `file.close()` right here."

Margaret walked over and examined his code:

```python
def process_logs(filenames):
    for filename in filenames:
        file = open(filename, 'r')
        data = file.read()
        if 'ERROR' in data:
            analyze_errors(data)
            return  # Found errors, exit early
        file.close()
```

"I see the problem," Margaret said gently. "When you `return` early, that `close()` never executes. The file stays open."

Timothy's eyes widened. "Oh no. So if I find an error in the first file, all the other files stay open?"

"Exactly. And if your analysis function raises an exception, the file stays open too. Let me show you Python's solution."

## The Problem: Manual Resource Management

"Resource management - files, database connections, locks - is tricky," Margaret explained. "You need to guarantee cleanup happens, even when things go wrong. That's what the `with` statement does."

She rewrote Timothy's function:

```python
def process_logs(filenames):
    for filename in filenames:
        with open(filename, 'r') as file:
            data = file.read()
            if 'ERROR' in data:
                analyze_errors(data)
                return
```

"Wait," Timothy said. "Where's the `close()`? Did we just... forget it?"

"No - the `with` statement handles it automatically. Let me show you the structure."

**Tree View:**

```python
process_logs(filenames)
    For filename in filenames
        With open(filename, 'r') as file
            data = file.read()
            If 'ERROR' in data
            ├── analyze_errors(data)
            └── Return
```

**English View:**

```python
Function process_logs(filenames):
  For each filename in filenames:
    With open(filename, 'r') as file:
      Set data to file.read().
      If 'ERROR' in data:
        Evaluate analyze_errors(data).
        Return.
```

Timothy studied the structure. "So the `with` block wraps everything that uses the file. But how does it know to close it?"

"The `with` statement guarantees cleanup," Margaret said. "When the block ends - whether normally, by return, by break, or even by exception - Python calls the file's cleanup code. Always."

## Understanding the Pattern

Margaret pulled up another example to make the guarantee more visible:

```python
def safe_write(filename, data):
    try:
        with open(filename, 'w') as f:
            f.write(data)
            raise ValueError("Something went wrong!")
    except ValueError:
        print("Caught error - but file was still closed")
```

**Tree View:**

```python
safe_write(filename, data)
    Try
        With open(filename, 'w') as f
            f.write(data)
            Raise ValueError('Something went wrong!')
    Except ValueError
        print('Caught error - but file was still closed')
```

**English View:**

```python
Function safe_write(filename, data):
  Try:
    With open(filename, 'w') as f:
      Evaluate f.write(data).
      Raise ValueError('Something went wrong!').
  Except ValueError:
    Evaluate print('Caught error - but file was still closed').
```

"See?" Margaret pointed at the structure. "Even though we raise an exception right after writing, the `with` statement ensures the file gets closed before the exception propagates. The cleanup happens automatically."

Timothy traced the flow with his finger. "So it's like an invisible `finally` block that always runs?"

"That's exactly what it is," Margaret confirmed. "Under the hood, `with` is essentially syntactic sugar for try/finally with some extra protocol handling. But the structure makes the guarantee explicit - everything in this block has managed cleanup."

## Managing Multiple Resources

"What if I need multiple files open at once?" Timothy asked.

Margaret showed him:

```python
def merge_files(input1, input2, output):
    with open(input1, 'r') as f1:
        with open(input2, 'r') as f2:
            with open(output, 'w') as out:
                out.write(f1.read())
                out.write(f2.read())
```

**Tree View:**

```python
merge_files(input1, input2, output)
    With open(input1, 'r') as f1
        With open(input2, 'r') as f2
            With open(output, 'w') as out
                out.write(f1.read())
                out.write(f2.read())
```

**English View:**

```python
Function merge_files(input1, input2, output):
  With open(input1, 'r') as f1:
    With open(input2, 'r') as f2:
      With open(output, 'w') as out:
        Evaluate out.write(f1.read()).
        Evaluate out.write(f2.read()).
```

"Look at the nesting," Margaret said. "The structure shows the cleanup order clearly. When the innermost block exits, `out` closes. Then `f2` closes. Then `f1` closes. Last-in, first-out - just like unwinding a stack."

Timothy nodded. "So even if the write fails, all three files get closed in the right order?"

"Exactly. Python 3.1 added a shortcut for this, though:"

```python
def merge_files(input1, input2, output):
    with open(input1, 'r') as f1, open(input2, 'r') as f2, open(output, 'w') as out:
        out.write(f1.read())
        out.write(f2.read())
```

**Tree View:**

```python
merge_files(input1, input2, output)
    With open(input1, 'r') as f1, open(input2, 'r') as f2, open(output, 'w') as out
        out.write(f1.read())
        out.write(f2.read())
```

**English View:**

```python
Function merge_files(input1, input2, output):
  With open(input1, 'r') as f1, open(input2, 'r') as f2, open(output, 'w') as out:
    Evaluate out.write(f1.read()).
    Evaluate out.write(f2.read()).
```

"Same guarantee, flatter structure," Margaret noted. "All three resources managed, all three guaranteed to close."

Timothy looked back at his original code with all those manual `close()` calls. "So every time I write `open()`, I should use `with`?"

"Not just files," Margaret said. "Database connections, network sockets, locks, threads - anything that needs cleanup. If an object supports the context manager protocol, you can use it with `with`."

"How do I know if something supports it?" Timothy asked.

"The documentation will say. And modern Python libraries almost always do - it's considered best practice. The `with` statement is how you signal 'I'm managing a resource here.'"

Timothy refactored his log processing function, wrapping each file operation in a `with` block. "No more leaked file handles. The structure guarantees it."

"Now you're thinking about resource lifetimes structurally," Margaret said with approval. "The indentation shows exactly how long each resource lives."

---

**Explore Python structure yourself:** Download the [Python Structure Viewer](https://www.pacificw.com) - a free tool that shows code structure in tree and plain English views. Works offline, no installation required.

![Python Structure Viewer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xklobpsq4sx3g4spefv.png align="left")

---

*Aaron Rose is a software engineer and technology writer at* [*tech-reader.blog*](https://www.tech-reader.blog) *and the author of* [*Think Like a Genius*](https://amazon.com/author/aaron.rose)*.*
