Skip to content

How to Use Python Context Managers for Safe File Operations?

Python programming

The Problem

I was writing a configuration file saver when the application crashed mid-write. The file was half-written and completely corrupted. The original content was gone. The new content was incomplete. No rollback. No recovery.

What happened to my config file
Before: {"database": "postgres", "port": 5432, "debug": false}
After crash: {"database": "postg # <-- corrupted, truncated here

The code I wrote was dangerously simple:

unsafe_config_save.py
def save_config(config, filename):
with open(filename, 'w') as f:
json.dump(config, f)
# If exception here, file is partially written!
validate_config(config) # This raised an exception

When validate_config() raised an exception, the file was already overwritten. The with statement closed the file handle properly, but the content was garbage. This is a classic problem with standard Python file writes.

Why Standard File Writes Are Dangerous

Standard Python open() writes immediately. There’s no buffering-attempt mechanism. The moment you call write(), bytes go to the file:

Timeline of a corrupted write
Time 0: open('config.json', 'w') --> file cleared to zero bytes
Time 1: write('{') --> file contains '{'
Time 2: write('"database"') --> file contains '{"database"'
Time 3: EXCEPTION RAISED --> file contains '{"database"' (corrupted)
Time 4: file closed --> no recovery possible

This pattern affects any file write that might fail mid-operation:

Common scenarios that corrupt files
- Configuration updates (app crashes during save)
- Data exports (validation fails after write starts)
- Log rotation (disk full mid-write)
- Database dumps (network timeout during write)
- Template rendering (parsing error after file opened)

I found a library called safer that solves this elegantly. It’s a drop-in replacement for open():

Install safer
pip install safer
safe_config_save.py
import safer
import json
def save_config(config, filename):
with safer.open(filename, 'w') as f:
json.dump(config, f)
validate_config(config) # If this raises, file unchanged!

The magic: safer.open() writes to a temporary file first. Only on successful completion does it replace the original. If any exception occurs, the original file stays untouched.

I tested this with an intentional crash:

test_safer_behavior.py
import safer
with safer.open('important.txt', 'w') as f:
f.write('oops')
raise ValueError("intentional crash")
# Result: 'important.txt' is unchanged (or doesn't exist)
# No partial write. No corruption.

The safer library handles edge cases I would miss:

safer handles these edge cases
- Disk full during write
- Permission errors
- Race conditions
- Proper temp file cleanup
- Atomic replace on success

safer with Streams (Not Just Files)

safer isn’t limited to files. It works with any callable:

safer_socket_example.py
import safer
import socket
def send_to_socket(host, port, data):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
# Only sends if entire operation succeeds
with safer.writer(sock.send) as send:
send(b'HEADER\n')
send(data)
send(b'\nFOOTER')
validate_data(data) # If raises, nothing sent
sock.close()

This pattern is useful for network operations where partial sends cause protocol errors.

Solution 2: Custom Context Manager (Zero Dependencies)

When I can’t add dependencies, I build a custom context manager. The @contextlib.contextmanager decorator makes this straightforward:

custom_safe_write.py
import contextlib
import io
from typing import Generator, IO
@contextlib.contextmanager
def write_if_success(real_fp: IO[bytes]) -> Generator[IO[bytes], None, None]:
"""
Buffer writes in memory, only commit to real file on success.
"""
buffer = io.BytesIO()
try:
yield buffer
# Only reached if no exception in the with block
real_fp.write(buffer.getvalue())
finally:
buffer.close()
# Usage:
def save_binary_data(data: bytes, filename: str):
with open(filename, 'wb') as real_fp:
with write_if_success(real_fp) as safe_fp:
safe_fp.write(data)
process_data(data) # If raises, file unchanged

The pattern is simple:

How the custom context manager works
1. Create in-memory buffer (BytesIO or StringIO)
2. Yield the buffer to the with block
3. User writes to buffer (not real file)
4. If no exception: write buffer to real file
5. If exception: buffer discarded, real file untouched

Custom Context Manager for Text Files

For text files with atomic replace:

safe_text_write.py
import contextlib
import io
import os
from typing import Generator
@contextlib.contextmanager
def safe_text_write(filename: str, encoding: str = 'utf-8') -> Generator[io.StringIO, None, None]:
"""
Safe text file write with automatic rollback.
Creates temp file, only replaces original on success.
"""
buffer = io.StringIO()
temp_filename = f"{filename}.tmp"
try:
yield buffer
# Write to temp file first
with open(temp_filename, 'w', encoding=encoding) as f:
f.write(buffer.getvalue())
# Replace original (atomic on most systems)
os.replace(temp_filename, filename)
finally:
buffer.close()
# Clean up temp file if it exists
if os.path.exists(temp_filename):
os.remove(temp_filename)
# Usage:
with safe_text_write('config.json') as f:
f.write(json.dumps(config))
validate_config(config) # If raises, original unchanged

Comparison: safer vs Custom Context Manager

I evaluated both approaches for different scenarios:

Feature comparison
| Feature | safer.open() | Custom Context Manager |
|---------------------|------------------------|------------------------|
| Zero dependencies | No (pip install) | Yes |
| Stream support | Yes (safer.writer) | Requires custom code |
| Edge case handling | Comprehensive | Must implement yourself|
| Learning curve | Low | Medium |
| Maintenance | Library handles | You maintain |
| Flexibility | High | Maximum |
| Battle-tested | Yes | No (your code) |

When to Use Each

Decision guide
Use safer when:
- Production code where reliability matters
- Working with streams or sockets
- Need comprehensive edge case handling
- Don't want to maintain custom code
Use custom context manager when:
- Can't add external dependencies
- Learning how context managers work
- Need specific customization
- Simple file writes with clear success/failure points

Common Mistakes to Avoid

Mistake 1: Not using any safety mechanism

dangerous_pattern.py
# WRONG: Direct write without safety
with open('config.json', 'w') as f:
json.dump(config, f)
do_something_risky() # Crash here = corrupted file

Mistake 2: Reinventing the wheel poorly

naive_attempt.py
# WRONG: Naive attempt without proper cleanup
try:
with open('config.json.tmp', 'w') as f:
json.dump(config, f)
os.rename('config.json.tmp', 'config.json')
except:
# Temp file left behind, original might be partially replaced
pass

This misses:

  • Disk full scenarios
  • Permission errors mid-write
  • Race conditions
  • Proper temp file cleanup on all failure paths

Mistake 3: Over-engineering simple cases

When simpler is better
For truly atomic writes where the entire content is ready:
- Just use os.replace() which is atomic
- Don't need full context manager complexity
But for writes that happen incrementally or might fail mid-stream,
safety mechanisms are essential.

Summary

Standard Python file writes are vulnerable to corruption on exceptions. Two solutions exist:

  1. safer library - Drop-in replacement for open(), handles all edge cases, works with streams. Use this for production code.

  2. Custom context manager - Zero dependencies, full control, learning opportunity. Use this when you can’t add dependencies or need specific customization.

The pattern in both: buffer writes until success, then commit. If failure, discard buffer, original untouched. This pattern transforms dangerous file writes into safe operations.

Audit your codebase for unsafe file writes. Start with configuration files and data exports where corruption would be most damaging.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments