Files and exceptions
Most programs that do real work touch the filesystem: reading a config, writing results, loading data. And when things go wrong, Python raises an exception, a signal that something unexpected happened. This chapter covers both: getting data in and out of files, and writing code that handles errors gracefully instead of crashing.
Opening files
open() opens a file and returns an object you can read from or write to. You tell it the path and what you want to do with the file (read, write, or append). Always close a file when you are done; the with statement does this automatically.
f = open("data.txt", "r") # "r" = read
content = f.read()
f.close()The "r" is the mode:
| Mode | Meaning |
|---|---|
"r" | Read. File must exist. Default mode. |
"w" | Write. Creates or overwrites the file. |
"a" | Append. Adds to the end without erasing. |
"x" | Create. Fails if the file already exists. |
"r+" | Read and write. |
"b" | Binary. Add to any mode: "rb", "wb". |
Always call .close() when you are done. Forgetting it leaves the file locked and can cause data corruption. The reliable way to handle this is the with statement.
The with statement
with open(...) manages the file for you, closing it automatically when the indented block finishes, even if an error happens. Always use with open(...) instead of manual open()/close(). It is safer and it is the standard.
with open("data.txt", "r") as f:
content = f.read()
# f is closed here, guaranteedWhat does with do?
with is Python's context manager syntax. It calls setup and teardown code for you; in this case, opening and reliably closing the file. You do not need to know how it works internally. Just use it with open().
Reading files
Three methods for reading. .read() loads the entire file as one string. .readline() reads one line. Iterating directly over the file object reads line by line, which is the most efficient approach for large files since it does not load everything into memory at once.
with open("data.txt", "r") as f:
content = f.read() # entire file as one string
with open("data.txt", "r") as f:
first_line = f.readline() # one line at a time
with open("data.txt", "r") as f:
lines = f.readlines() # list of lines, each ending in "\n"For large files, reading line by line is more efficient than loading everything at once:
with open("big_file.txt", "r") as f:
for line in f: # iterate the file directly, memory-efficient
print(line.strip()) # strip() removes the trailing newlineIterating directly over the file object (for line in f) is the most efficient and idiomatic way to read a large file.
Writing files
"w" mode overwrites the file entirely if it exists. "a" mode adds to the end. .write() does not add a newline automatically; include "\n" explicitly at the end of each line. To write multiple lines at once, join them with "\n".join().
with open("output.txt", "w") as f:
f.write("Hello, world\n")
with open("output.txt", "a") as f:
f.write("Another line\n")"w" overwrites the file entirely if it exists. "a" adds to the end.
f.write() does not add a newline automatically, so include "\n" explicitly. To write multiple lines at once:
lines = ["Line one", "Line two", "Line three"]
with open("output.txt", "w") as f:
f.write("\n".join(lines) + "\n")Exceptions
When Python hits a problem it cannot handle, it raises an exception: an error that describes what went wrong and where. If you do not handle it, your program crashes and prints a traceback. The table below shows the most common exceptions you will encounter.
Common exceptions you will encounter:
| Exception | When it happens |
|---|---|
FileNotFoundError | open() cannot find the file |
ValueError | Function gets a value of the right type but wrong content, e.g. int("abc") |
TypeError | Wrong type entirely, e.g. "hello" + 5 |
KeyError | Dictionary key does not exist |
IndexError | List index out of range |
ZeroDivisionError | Division by zero |
AttributeError | Object does not have that attribute or method |
try / except
Wrap code that might fail in a try block. If an exception occurs, the matching except block handles it instead of crashing. Be specific about which exception you catch: catching everything with a bare except: hides real bugs.
try:
value = int("abc")
except ValueError:
print("That's not a valid number")Be specific about which exception you catch. Catching all exceptions with a bare except: hides bugs:
# bad, catches everything including programmer mistakes
try:
result = do_something()
except:
pass
# good, only catches what you expect and can actually handle
try:
result = do_something()
except FileNotFoundError:
print("File not found")Catching multiple exceptions
You can handle different error types in separate except blocks, or catch several types in one block using a tuple. The as e part gives you access to the error message.
try:
data = int(user_input)
result = 100 / data
except ValueError:
print("Not a number")
except ZeroDivisionError:
print("Can't divide by zero")Or catch multiple in a tuple:
except (ValueError, ZeroDivisionError) as e:
print(f"Input error: {e}")as e binds the exception object to a name so you can inspect the message.
else and finally
else runs only if no exception occurred. finally always runs, whether or not there was an exception. finally is useful for cleanup that must happen no matter what.
try:
with open("data.txt") as f:
content = f.read()
except FileNotFoundError:
print("File not found, using defaults")
content = ""
else:
print("File loaded successfully")
finally:
print("Done attempting to load file") # always runsfinally is most useful for cleanup (closing connections, releasing locks) even when you are already using with for files.
raise
You can raise exceptions yourself with raise. This is how you make your functions signal problems clearly to callers instead of silently returning a wrong value.
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / bThis makes your functions explicit about what they expect and signals problems clearly to callers.
Custom exception classes
For larger programs, you can define your own exception types by inheriting from Exception. This lets callers catch your specific errors separately from other kinds of errors.
class InsufficientFundsError(Exception):
pass
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount}, balance is {self.balance}"
)
self.balance -= amounttry:
account.withdraw(1000)
except InsufficientFundsError as e:
print(f"Transaction declined: {e}")JSON
JSON is the format that everything speaks: APIs, config files, data exports. Python's json module handles it directly. json.load() reads JSON from a file into a Python dict or list. json.dump() writes a Python dict or list to a file as JSON.
Read JSON from a file:
import json
with open("config.json", "r") as f:
config = json.load(f) # parses JSON into a Python dict/list
print(config["setting"])Write JSON to a file:
import json
data = {"name": "Alice", "score": 87, "active": True}
with open("output.json", "w") as f:
json.dump(data, f, indent=2) # indent= makes it human-readableJSON to Python type mapping:
| JSON | Python |
|---|---|
object {} | dict |
array [] | list |
string "" | str |
| number | int or float |
true / false | True / False |
null | None |
To convert between JSON strings and Python objects without touching a file:
import json
# string to Python
data = json.loads('{"name": "Alice", "score": 87}')
# Python to string
text = json.dumps({"name": "Alice", "score": 87}, indent=2)json.load() reads from a file object. json.loads() (with an "s") reads from a string.
In practice
A save/load pattern for a simple game: write state to JSON, load it back on the next run, and fall back to defaults if no save file exists yet:
import json
SAVE_FILE = "save_game.json"
def save_game(player_data: dict) -> None:
with open(SAVE_FILE, "w") as f:
json.dump(player_data, f, indent=2)
print("Game saved.")
def load_game() -> dict:
try:
with open(SAVE_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
print("No save file found, starting fresh.")
return {"name": "Player", "score": 0, "level": 1}
state = load_game()
state["score"] += 50
save_game(state)
