File I/O & Exception Handling
After this lesson, you will be able to:
- Open, read, and write files using Python's open() function
- Use the with statement for safe, automatic file handling
- Handle errors gracefully with try/except/finally blocks
- Raise and create custom exceptions for meaningful error messages
- Parse structured text data like CSV without external libraries
What does this code print? try: x = int('abc') except ValueError: print('Bad value') finally: print('Done')
Before You Start
#Reading Files
open() function opens a file and returns a file object.# Basic file reading
file = open("data.txt", "r") # "r" = read mode (default)
content = file.read() # read the entire file as one string
print(content)
file.close() # ALWAYS close when doneopen() and close(), the file never gets closed. This can corrupt data and leak system resources.The with Statement (Always Use This)
# The safe way -- with statement handles closing automatically
with open("data.txt", "r") as file:
content = file.read()
print(content)
# file is automatically closed here, even if an error occurswith statement is called a context managerContext ManagersContext managers define __enter__ and __exit__ for the with statement, guaranteeing cleanup (closing files, releasing connections) even when exceptions occur.Learn more →. It guarantees the file is closed when the block ends, no matter what happens. Always use with for file operations.How with Actually Works (the __enter__ / __exit__ Protocol)
with cm() as x: is equivalent to a try / finally: Python first calls cm.__enter__() (which sets things up and returns whatever you bind to x), then runs the body, then ALWAYS calls cm.__exit__() — even when the body raises an exception. That's the whole point. The animation below walks through five scenarios so you can see it move:- A — Normal: enter → body → exit, in order.
- B — Exception in body: the body crashes mid-flight;
__exit__STILL runs and closes the file before the exception propagates up. This is the killer feature. - C —
__exit__returnsTrue: the exit hook swallows the exception entirely and execution continues past thewithblock. (Powerful but rare — usually you let exceptions through.) - D — Nested
with: two stacked blocks; cleanup is LIFO — the innermost resource is released first, like a call stack popping. - E —
@contextmanager: the generatorGenerators & yieldA generator uses yield instead of return to produce values lazily, one at a time. This keeps memory usage constant even for infinite or very large sequences.Learn more →-based shortcut. Code beforeyieldis__enter__, code in thefinallyafteryieldis__exit__.
with is not a syntactic nicety — it's an exception-safe cleanup contract baked into the language. Anything that owns a resource (a file, a network socket, a database transaction, a lock) should expose itself as a context manager so callers can use it inside with and never leak.#Reading Methods
# Read entire file as one string
with open("data.txt", "r") as f:
all_text = f.read()
# Read all lines into a list
with open("data.txt", "r") as f:
lines = f.readlines() # ["line 1\n", "line 2\n", ...]
# Read line by line (memory-efficient for large files)
with open("data.txt", "r") as f:
for line in f:
print(line.strip()) # .strip() removes the trailing newline.read() would consume all your RAM.A file contains: hello world What does this print? with open("data.txt") as f: for line in f: print(line)
HitFileNotFoundError,PermissionError, orIsADirectoryError? These are subclasses ofOSErrorand crashopen()when the path is wrong, the file is locked, or you pointed at a folder. Wrapopen()intry/except— see the error decoder for the full catalog andImportError-style import crashes too.
#Writing Files
# Write mode ("w") -- creates file or OVERWRITES existing content
with open("output.txt", "w") as f:
f.write("First line\n")
f.write("Second line\n")
# Append mode ("a") -- adds to the end without erasing
with open("output.txt", "a") as f:
f.write("Third line (appended)\n")
# Write multiple lines at once
lines = ["Alice,95\n", "Bob,87\n", "Charlie,92\n"]
with open("grades.csv", "w") as f:
f.writelines(lines)#File Modes Summary
| Mode | Description | Creates file? | Erases existing? |
|---|---|---|---|
"r" | Read only | No | No |
"w" | Write (overwrite) | Yes | Yes |
"a" | Append | Yes | No |
"r+" | Read and write | No | No |
"x" | Create (fails if exists) | Yes | No |
What happens if you open an existing file with mode 'w' and write one line?
#Parsing Structured Data
A very common task is reading CSV-like data from files or strings:
# Parse CSV data without any external library
csv_data = """name,age,score
Alice,17,95
Bob,16,87
Charlie,18,92
Diana,17,78
Eve,16,99"""
# Split into lines, then split each line by comma
lines = csv_data.strip().split("\n")
header = lines[0].split(",")
students = []
for line in lines[1:]:
values = line.split(",")
student = {
"name": values[0],
"age": int(values[1]),
"score": int(values[2]),
}
students.append(student)
# Now we can work with structured data
for s in students:
status = "PASS" if s["score"] >= 80 else "FAIL"
print(f"{s['name']:>10} (age {s['age']}): {s['score']} - {status}")
# Filter and analyze
top_students = [s for s in students if s["score"] >= 90]
avg_score = sum(s["score"] for s in students) / len(students)
print(f"\nAverage score: {avg_score:.1f}")
print(f"Top students: {[s['name'] for s in top_students]}")#Working with JSON
json moduleModules & PackagesA module is a .py file you import to reuse code. Use import, from...import, and aliases (import numpy as np). Packages are directories with __init__.py.Learn more → handles JSON files, which are extremely common for configurations and API responses:import json
# Python dict to JSON string
config = {
"theme": "dark",
"font_size": 14,
"autosave": True,
"language": "en",
}
json_string = json.dumps(config, indent=2)
print(json_string)
# JSON string back to Python dict
loaded = json.loads(json_string)
print(loaded["theme"]) # dark
# Save to file
with open("config.json", "w") as f:
json.dump(config, f, indent=2)
# Load from file
with open("config.json", "r") as f:
loaded_config = json.load(f)#Exception Handling
# This crashes with FileNotFoundError
# with open("nonexistent.txt") as f:
# data = f.read()
# This crashes with ZeroDivisionError
# result = 10 / 0
# This crashes with ValueError
# number = int("not_a_number")#try / except
# Catch specific errors
try:
with open("data.txt", "r") as f:
content = f.read()
print("File read successfully!")
except FileNotFoundError:
print("Error: File not found. Using default data.")
content = "default data"
except PermissionError:
print("Error: No permission to read the file.")
content = ""
print(f"Content: {content}")#try / except / else / finally
def safe_divide(a, b):
"""Divide a by b with error handling."""
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero!")
return None
except TypeError:
print(f"Invalid types: {type(a)} and {type(b)}")
return None
else:
# Runs ONLY if no exception occurred
print(f"{a} / {b} = {result}")
return result
finally:
# Runs ALWAYS, whether or not an exception occurred
print("Division operation complete.\n")
safe_divide(10, 3) # 10 / 3 = 3.333... then "complete"
safe_divide(10, 0) # "Cannot divide by zero!" then "complete"
safe_divide("a", 2) # "Invalid types..." then "complete"#Common Exception Types
| Exception | When It Occurs |
|---|---|
FileNotFoundError | File does not exist |
PermissionError | No permission to read/write |
ValueError | Wrong value type (e.g., int("abc")) |
TypeError | Wrong operation on a type (e.g., "a" + 1) |
KeyError | Dictionary key does not exist |
IndexError | List index out of range |
ZeroDivisionError | Division by zero |
AttributeError | Object does not have the attribute/method |
#Custom Exceptions and Raising Errors
You can create your own exception types and raise them intentionally:
# Custom exception
class InvalidScoreError(Exception):
"""Raised when a score is outside the valid range."""
pass
class DataValidationError(Exception):
"""Raised when input data fails validation."""
def __init__(self, field, value, message):
self.field = field
self.value = value
super().__init__(f"Validation failed for '{field}' (value: {value}): {message}")
def validate_score(name, score):
"""Validate a student's score."""
if not isinstance(score, (int, float)):
raise DataValidationError("score", score, "must be a number")
if score < 0 or score > 100:
raise InvalidScoreError(f"{name}'s score {score} is out of range (0-100)")
return True
# Using custom exceptions
students_raw = [
("Alice", 95),
("Bob", -5),
("Charlie", "ninety"),
("Diana", 88),
]
valid_students = []
for name, score in students_raw:
try:
validate_score(name, score)
valid_students.append({"name": name, "score": score})
except InvalidScoreError as e:
print(f"Skipping: {e}")
except DataValidationError as e:
print(f"Skipping: {e}")
print(f"\nValid students: {len(valid_students)} out of {len(students_raw)}")
for s in valid_students:
print(f" {s['name']}: {s['score']}")#A Real-World Pattern: Safe Data Loading
def load_dataset(filepath):
"""Load and parse a CSV dataset with comprehensive error handling."""
rows = []
errors = []
try:
with open(filepath, "r") as f:
lines = f.readlines()
except FileNotFoundError:
print(f"Dataset not found: {filepath}")
return [], ["File not found"]
if len(lines) < 2:
return [], ["File is empty or has no data rows"]
header = lines[0].strip().split(",")
for i, line in enumerate(lines[1:], start=2):
try:
values = line.strip().split(",")
if len(values) != len(header):
raise ValueError(f"Expected {len(header)} columns, got {len(values)}")
row = dict(zip(header, values))
rows.append(row)
except ValueError as e:
errors.append(f"Row {i}: {e}")
print(f"Loaded {len(rows)} rows ({len(errors)} errors)")
if errors:
for err in errors[:5]: # show first 5 errors
print(f" Warning: {err}")
return rows, errors#Code Playground
Tests · Parse the CSV, handle errors gracefully, and verify your error messages are clear!
#Saving Your Program's Own Data
Sometimes you need to save something that is not simple text — a whole Python object, exactly as it is, so you can load it back later.
import pickle, json
# A program's state: whatever you want to survive after it closes
game_state = {
"player": "Asha",
"level": 7,
"inventory": ["torch", "rope", "map"],
"position": (14, 22),
}
# Option 1: JSON -- human-readable, and any language can read it.
# Works for lists, dicts, strings, numbers, booleans.
with open("save.json", "w") as f:
json.dump(game_state, f, indent=2)
# Option 2: pickle -- stores almost ANY Python object, including tuples
# and custom classes, but only Python can read it back.
with open("save.pkl", "wb") as f:
pickle.dump(game_state, f)
# --- Load it back ---
with open("save.json") as f:
from_json = json.load(f)
with open("save.pkl", "rb") as f:
from_pickle = pickle.load(f)
print(from_json["position"]) # [14, 22] <- a LIST, tuples become lists
print(from_pickle["position"]) # (14, 22) <- still a tupleThis program writes a note to a file and then reads it back. But the read returns an empty string because the writer was never closed. Fix it using a context manager.
Got: hello from python
#Key Takeaways
- Always use
withfor file operations — thewith open(path) as f:pattern guarantees the file is closed properly, even if an error occurs. Never use bareopen()withoutwith - Know your file modes —
"r"reads,"w"overwrites (dangerous!),"a"appends. Accidentally using"w"on an important file erases everything - try/except catches errors without crashing — wrap risky operations in
try, handle specific exceptions inexcept, usefinallyfor cleanup that must always run - Catch specific exceptions, not all exceptions —
except ValueErroris good. Bareexcept:is bad because it hides bugs by catching everything, even typos in your code - Custom exceptions make your code self-documenting —
raise InvalidScoreError(...)is far more helpful thanraise ValueError("bad")when debugging at 2 AM
What is the main advantage of using 'with open(...)' over plain 'open()'?