Skip to main content

Files and exceptions: reading the world, handling failure

So far your programs have been sealed boxes — the data lived in variables you typed yourself, and you assumed nothing would ever go wrong. Real programs are not like that. They read data from files the user provides, and they deal gracefully when something fails — a missing file, bad input, a network hiccup. This lesson gives you both halves of that: how to get text in and out of files, and how to handle errors on purpose instead of letting your program crash. These two skills travel together because the moment you touch the outside world, things can fail.

Reading a text file

A file is data stored on disk under a name, like notes.txt. To work with one in Python you open it, which gives you a file object you can read from or write to. The built-in is open(filename, mode):

f = open("notes.txt", "r") # "r" = read mode
contents = f.read() # read the WHOLE file as one string
f.close() # always close what you open
print(contents)

The second argument is the mode — what you intend to do:

  • "r"read (the default). The file must already exist.
  • "w"write. Creates the file, or erases it if it exists, then writes fresh.
  • "a"append. Adds to the end of the file, keeping what's there.

Reading the whole file at once with .read() is fine for small files. More often you want it line by line, which you get by looping over the file object directly:

f = open("notes.txt", "r")
for line in f: # the file hands you one line at a time
print(line.strip()) # .strip() removes the trailing newline
f.close()

:::note Why .strip()? Each line read from a file keeps its trailing newline character (\n) — that's what separates lines on disk. line.strip() trims whitespace (including that newline) off both ends, so your printed output isn't double-spaced. You met .strip() back in the strings lesson; this is where it earns its keep. :::

The with statement: never forget to close

Every open() must be matched by a close(), or you leak resources — and if your code crashes between the two, close() never runs. The fix is the with statement, which opens the file, gives it to you for the indented block, and closes it automatically when the block ends, even if an error happens inside:

with open("notes.txt", "r") as f:
contents = f.read()
print(contents)
# file is automatically closed here — no f.close() needed

Read with open(...) as f: as "open this file, call it f for the duration of this block, and close it for me when I'm done." This is the standard, correct way to handle files — you'll almost never write a bare open/close pair in real code. Use with every time.

Writing a file

Writing uses "w" (overwrite) or "a" (append) mode and the .write() method. Note that .write() does not add a newline for you — you include \n yourself when you want one:

with open("output.txt", "w") as f:
f.write("First line\n")
f.write("Second line\n")
# output.txt now contains those two lines

# append more without erasing
with open("output.txt", "a") as f:
f.write("Third line\n")

A common pattern is writing a list of strings, one per line:

names = ["Ada", "Lin", "Sam"]
with open("names.txt", "w") as f:
for name in names:
f.write(name + "\n")

:::warning "w" erases first Opening a file in "w" mode wipes it immediately, before you write a single character. If you meant to add to an existing file, use "a". Reaching for "w" on a file you cared about is a classic, painful beginner mistake. :::

Handling failure: try / except

Now the other half. What happens when you try to open a file that doesn't exist?

with open("missing.txt", "r") as f:
print(f.read())
# FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'

Your program crashes. Sometimes that's fine. But often you want to catch the failure and respond — show a friendly message, use a default, try something else. That is what try / except is for. You put the risky code in a try block; if an exception (an error that stops normal flow) happens, Python jumps to the matching except block instead of crashing:

try:
with open("missing.txt", "r") as f:
print(f.read())
except FileNotFoundError:
print("No such file — using defaults instead.")
# prints: No such file — using defaults instead.

The program continues instead of dying. Read it as: "try this; if a FileNotFoundError happens, do this instead."

:::note Catch SPECIFIC exceptions Always name the exception type you expect, like except FileNotFoundError: or except ValueError:. You can write a bare except: that catches everything, but don't — it hides bugs you didn't anticipate (a typo'd variable name would be silently swallowed). Catch the specific failures you know how to handle, and let unexpected ones surface loudly. You already learned these type names — ValueError, KeyError, IndexError, ZeroDivisionError — back in the debugging lesson; now you can catch them. :::

You can catch several specific types, and you can capture the error object itself with as to inspect its message:

try:
age = int(input_text) # might be ValueError if not a number
result = 100 / age # might be ZeroDivisionError if age is 0
except ValueError:
print("That wasn't a number.")
except ZeroDivisionError:
print("Age can't be zero.")
except (TypeError, KeyError) as e: # catch several types; e holds the error
print("Something else went wrong:", e)

else and finally: the full shape

Two optional companions complete the picture:

  • else runs only if the try block finished with no exception. It's where you put "the rest of the happy path," kept separate from the risky line.
  • finally runs no matter what — exception or not, caught or not. It's for cleanup that must always happen.
try:
f = open("data.txt", "r")
contents = f.read()
except FileNotFoundError:
print("File missing.")
else:
print("Read", len(contents), "characters.") # only if the try succeeded
finally:
print("Done attempting to read.") # ALWAYS runs

The order is fixed: tryexceptelsefinally. In everyday code you'll most often use just try/except; else and finally appear when you want that precise control over the success path and guaranteed cleanup. (with already handles file cleanup, which is one big reason it's preferred — it's finally-for-files, built in.)

raise: throw your own error

Sometimes your code is the one that should refuse. When a function gets input it cannot accept, the right move is often to raise an exception yourself — to deliberately trigger an error — rather than return a wrong answer or limp along silently:

def set_age(age):
if age < 0:
raise ValueError("age cannot be negative")
return age

set_age(25) # fine, returns 25
set_age(-3) # ValueError: age cannot be negative

raise ValueError("...") stops the function and signals failure with a clear message — which the caller can then except. This is how libraries tell you you've misused them, and how you protect your own functions from bad data. Raising early with a clear message beats returning garbage that breaks mysteriously ten lines later.

Why it matters

Reading and writing files is how programs persist anything — configs, logs, saved data, the JSON you'll parse next lesson. And exception handling is the difference between a script that explodes on the first surprise and a program that survives the real, messy world. The next guides assume this fluency everywhere: an API call wrapped in try/except for when the network fails, a config file opened with with, a raise when a request is malformed. You now handle both the outside world and its inevitable failures on purpose.

Where this leads: every robust program is risky operations wrapped in try/except — exactly the pattern that keeps real services alive. See Where this leads next.

Practice

Pyodide gives you a real in-browser file system, so open() actually works here. Write a function that writes lines to a file and reads them back, with error handling:

⌨️ Challenge — Python · practice (not graded)

Write save_and_count(lines) that: (1) writes each string in the list `lines` to a file called 'data.txt', one per line; (2) reads the file back and returns the number of non-empty lines it contains. Use `with open(...)` for both. For an empty list, the file is empty and you return 0.

Checkpoint

Required checkpoint

Files and exceptions

Pass to unlock the Next button below

Next: Classes & objects →