Python22 min readIntermediate

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 None

Multiple 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
💡 Tip
Don't use exceptions for control flow when an `if` would do. Exceptions are for EXCEPTIONAL cases — things that aren't supposed to happen in the normal path.