Files, Errors, and Context Managers
Read and write files safely, handle exceptions, and understand the `with` statement.
Reading and writing files
Files are the most common way programs communicate with the outside world — config files, log files, CSV exports, downloaded JSON. Python's built-in `open` function is your gateway to all of them.
# Writing a file
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("Line 1\n")
f.write("Line 2\n")
# Reading a file line by line (memory-efficient for huge files)
with open("notes.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.rstrip())The second argument to `open` is the mode: `"r"` for read (default), `"w"` for write (truncates the file!), `"a"` for append, and `"b"` appended for binary mode (e.g. `"rb"`).
Always specify `encoding="utf-8"` when working with text. The platform default differs between Windows and Linux, and that's a real source of "works on my machine" bugs.
Context managers and the `with` statement
Notice the `with` keyword. A context manager is an object that runs setup code when you enter a block and cleanup code when you leave it — even if an exception is raised. For files, the cleanup is closing the file. Without `with`, you'd have to remember to call `f.close()`, and if an exception happened in between, you'd leak the file handle.
# OK — file always closes, even on errors
with open("data.txt") as f:
contents = f.read()
# Bad — close() never runs if read() raises
f = open("data.txt")
contents = f.read()
f.close()Errors and exceptions
When something goes wrong — a missing file, bad input, network failure — Python raises an exception. An unhandled exception crashes your program and prints a traceback. To recover gracefully, wrap the risky code in a `try` block and catch the exception in `except`.
def parse_int(s):
try:
return int(s)
except ValueError as e:
print(f"\"{s}\" is not a valid integer: {e}")
return None
parse_int("42") # 42
parse_int("hello") # prints error, returns NoneMultiple except, else, finally
try:
with open("config.json") as f:
data = f.read()
except FileNotFoundError:
print("No config — using defaults")
data = ""
except PermissionError:
print("Can\u2019t read config — bailing out")
raise
else:
print(f"Loaded {len(data)} bytes")
finally:
print("Done attempting to load config")- `except` — catches a specific exception type. List the most specific first.
- `else` — runs only if no exception was raised in `try`.
- `finally` — always runs, exception or not. Great for cleanup.
- Bare `except:` (without a type) is a smell — it swallows EVERYTHING, including KeyboardInterrupt. Catch what you actually expect.
Raising your own exceptions
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"insufficient funds: {balance} < {amount}")
return balance - amount