Lesson 2: File I/O Fundamentals with Context Managers
Why Files Matter: From Console to Persistent Storage
So far, you've learned how to gather input from users and display output on the console. But there's a fundamental problem with console interaction: everything disappears when the program ends. When you close your terminal, all the data vanishes.
In the real world, applications need to persist data—save it in a way that survives beyond the program's execution. That's where files come in. A file is simply data stored on your computer's disk that exists independently of any running program. Think of it like the difference between writing a message on a whiteboard (console—temporary) versus writing it in a notebook (file—persistent).
This lesson teaches you how to safely read from and write to files using Python's most important file-handling pattern: context managers. By the end of this lesson, you'll understand why context managers matter and how to use them to prevent data loss and resource leaks.
Concept: Context Managers and Why They're Essential
Let's start with the most important pattern in file I/O: the context manager, accessed using Python's with statement.
The Problem: What Happens Without Context Managers
Without context managers, you might open a file like this:
# DANGEROUS: Don't do this (for illustration only)
file = open("notes.txt", 'r')
content = file.read()
file.close() # Easy to forget!
This approach has serious problems:
- Easy to forget closing: If your code crashes or takes an unexpected path, the
file.close()might never execute - Resource leaks: The operating system tracks open files. Leave too many open, and your program crashes
- Data corruption: Writing to files without properly closing them can lose data
The Solution: Context Managers with with
Context managers automatically handle setup and cleanup:
# SAFE: Always use this pattern
with open("notes.txt", 'r', encoding='utf-8') as file:
content = file.read()
# File is automatically closed here, even if error occurs
Three crucial points:
- Automatic cleanup: The
withstatement guarantees the file closes, even if an error occurs - Cleaner syntax: Less boilerplate code than manual open/close
- Exception safe: If an exception is raised inside the block, the file still closes properly
💬 AI Colearning Prompt
"Explain what happens if I forget the
withstatement and usefile = open(filename)then try to read it. Why iswithsafer?"
Expected Outcome: You'll understand context manager benefits—guaranteed cleanup that prevents file corruption.
Concept: File Modes Explained
Every time you open a file, you must specify a file mode that determines what you're allowed to do:
| Mode | Name | Behavior | File State |
|---|---|---|---|
'r' | Read | Read existing file | File must exist; not modified |
'w' | Write | Create new or overwrite | Existing content deleted; creates if doesn't exist |
'a' | Append | Add to end of file | Existing content preserved; creates if doesn't exist |
'r+' | Read + Write | Read and modify in place | File must exist; position matters |
Critical difference: 'w' deletes existing content. If the file exists and you open it in write mode, everything gets erased immediately.
Let me show you what this means in practice:
# File initially doesn't exist
# Open in write mode → file is created
with open("example.txt", 'w', encoding='utf-8') as f:
f.write("First line\n")
f.write("Second line\n")
# File now contains 2 lines
# Open same file in write mode again → **content is deleted**
with open("example.txt", 'w', encoding='utf-8') as f:
f.write("New content\n")
# File now contains only the new content (previous data is gone!)
# Open in append mode → add without deleting
with open("example.txt", 'a', encoding='utf-8') as f:
f.write("Appended line\n")
# File now contains: "New content\n" + "Appended line\n"
🎓 Instructor Commentary
In AI-native development, you don't memorize file mode combinations—you understand your INTENT (create new, append, read, modify). Ask AI: "I want to add lines to an existing file without losing data—which mode?" Your job: specify intent; syntax is cheap.
Concept: Reading Methods—Different Approaches for Different Needs
Python provides three ways to read file content, each with different purposes:
read() — Read Entire File at Once
with open("notes.txt", 'r', encoding='utf-8') as f:
content: str = f.read()
# content is now a single string containing entire file
print(content)
Use when: You want the entire file as one string; file is small enough to fit in memory.
readline() — Read One Line at a Time
with open("notes.txt", 'r', encoding='utf-8') as f:
line1: str = f.readline() # "First line\n"
line2: str = f.readline() # "Second line\n"
line3: str = f.readline() # "Third line\n"
Use when: You want to process the file line-by-line; useful for large files.
readlines() — Read All Lines into a List
with open("notes.txt", 'r', encoding='utf-8') as f:
lines: list[str] = f.readlines()
# lines = ["First line\n", "Second line\n", "Third line\n"]
for line in lines:
print(f"Line: {line.strip()}") # strip() removes \n
Use when: You want all lines as a list but want to iterate or process them later.
Note on newlines: Each line from readline() and readlines() includes the trailing \n. Use .strip() to remove it:
with open("notes.txt", 'r', encoding='utf-8') as f:
for line in f: # You can iterate directly
clean_line: str = line.strip()
print(f"Content: {clean_line}")
🚀 CoLearning Challenge
Ask your AI Co-Teacher:
"I have a 1GB log file. I need to find all lines containing 'ERROR'. Why would
readlines()be a bad choice? How would I process it more efficiently?"
Expected Outcome: You'll understand memory implications and learn to iterate file directly.
Concept: Writing Files with Proper Formatting
Writing to files requires careful attention to newlines. Unlike print() which automatically adds \n, the write() method writes exactly what you give it:
# Without explicit newlines, everything runs together
with open("output.txt", 'w', encoding='utf-8') as f:
f.write("Line 1") # No newline!
f.write("Line 2") # These concatenate
f.write("Line 3")
# File contents: "Line 1Line 2Line 3" (all on one line)
Correct approach: Always include '\n' explicitly:
with open("output.txt", 'w', encoding='utf-8') as f:
f.write("Line 1\n")
f.write("Line 2\n")
f.write("Line 3\n")
# File contents:
# Line 1
# Line 2
# Line 3
For multiple lines, use writelines():
lines: list[str] = [
"First note\n",
"Second note\n",
"Third note\n",
]
with open("notes.txt", 'w', encoding='utf-8') as f:
f.writelines(lines)
Concept: File Exceptions—Handling Errors Gracefully
When working with files, several errors can occur. The most common are:
FileNotFoundError
Raised when you try to open a file that doesn't exist:
with open("nonexistent.txt", 'r', encoding='utf-8') as f:
content = f.read()
# Raises: FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.txt'
PermissionError
Raised when you don't have permission to read or write a file:
with open("/root/private.txt", 'r', encoding='utf-8') as f:
content = f.read()
# Raises: PermissionError: [Errno 13] Permission denied: '/root/private.txt'
IOError
Generic I/O error (read errors, disk full, etc.):
# Disk full, or other I/O issue
with open("bigfile.txt", 'w', encoding='utf-8') as f:
f.write("x" * 10000000)
# Raises: IOError: No space left on device
Always wrap file operations in try/except:
try:
with open("data.txt", 'r', encoding='utf-8') as f:
content = f.read()
except FileNotFoundError:
print("Error: File not found. Creating a new one.")
with open("data.txt", 'w', encoding='utf-8') as f:
f.write("Initial content\n")
except PermissionError:
print("Error: You don't have permission to read this file.")
except IOError as e:
print(f"Error reading file: {e}")
✨ Teaching Tip
Use Claude Code to explore file errors: "What happens if I try to open a file that doesn't exist? Show me the actual error and how to handle it gracefully."
Code Examples
Code Example 2.1: Safe File Reading with Context Manager
Specification Reference: Demonstrate safe file reading with automatic cleanup; handle FileNotFoundError AI Prompt Used: "Show me how to safely open and read a text file with error handling"
def read_file_safely(filename: str) -> str | None:
"""Read file contents safely, return None if file doesn't exist."""
try:
with open(filename, 'r', encoding='utf-8') as file:
content: str = file.read()
print(f"Successfully read {len(content)} characters from {filename}")
return content
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
return None
except IOError as e:
print(f"Error reading file: {e}")
return None
# Test
result: str | None = read_file_safely("notes.txt")
if result:
print(f"Content:\n{result}")
else:
print("Could not read file")
Validation Steps:
- ✓ Context manager (
withstatement) ensures file is closed - ✓ Exception handling catches both missing files and I/O errors
- ✓ Returns meaningful data (
str | Nonetype hint) - ✓ Function is reusable across different filenames
Code Example 2.2: Different File Modes and Reading Methods
Specification Reference: Demonstrate write mode, multiple reading methods, newline handling AI Prompt Used: "Show me how to write a file with multiple lines, then read it back in different ways"
# Create a sample file with multiple lines
filename: str = "data.txt"
# WRITE MODE: Create new file or overwrite existing
with open(filename, 'w', encoding='utf-8') as f:
f.write("Alice,25,Engineering\n")
f.write("Bob,30,Sales\n")
f.write("Carol,28,Marketing\n")
print(f"Created {filename} with 3 lines\n")
# METHOD 1: read() - entire file as one string
print("=== Using read() ===")
with open(filename, 'r', encoding='utf-8') as f:
full_content: str = f.read()
print(f"File contents:\n{full_content}")
# METHOD 2: readline() - one line at a time
print("=== Using readline() ===")
with open(filename, 'r', encoding='utf-8') as f:
line1: str = f.readline().strip() # strip() removes \n
line2: str = f.readline().strip()
line3: str = f.readline().strip()
print(f"Line 1: {line1}")
print(f"Line 2: {line2}")
print(f"Line 3: {line3}")
# METHOD 3: readlines() - all lines as list
print("=== Using readlines() ===")
with open(filename, 'r', encoding='utf-8') as f:
lines: list[str] = f.readlines()
print(f"Total lines: {len(lines)}")
for i, line in enumerate(lines, 1):
print(f" Line {i}: {line.strip()}")
# METHOD 4: iterate directly (most Pythonic)
print("=== Iterating directly ===")
with open(filename, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
name, age, dept = line.strip().split(',')
print(f" {i}. {name} ({age}) - {dept}")
Output:
Created data.txt with 3 lines
=== Using read() ===
File contents:
Alice,25,Engineering
Bob,30,Sales
Carol,28,Marketing
=== Using readline() ===
Line 1: Alice,25,Engineering
Line 2: Bob,30,Sales
Line 3: Carol,28,Marketing
=== Using readlines() ===
Total lines: 3
Line 1: Alice,25,Engineering
Line 2: Bob,30,Sales
Line 3: Carol,28,Marketing
=== Iterating directly ===
1. Alice (25) - Engineering
2. Bob (30) - Sales
3. Carol (28) - Marketing
Validation Steps:
- ✓ Write mode creates file with multiple lines
- ✓ All four reading methods produce correct output
- ✓ Newline handling works correctly (
.strip()removes\n) - ✓ Direct iteration is most efficient
Code Example 2.3: Append Mode and File Growth
Specification Reference: Demonstrate append mode, contrast with write mode, show log file pattern AI Prompt Used: "Show me how to add lines to a file without deleting existing content"
filename: str = "log.txt"
# INITIAL WRITE: Create the log file
print("=== Creating log file ===")
with open(filename, 'w', encoding='utf-8') as f:
f.write("2025-11-09 10:00 - Program started\n")
f.write("2025-11-09 10:05 - User login\n")
print("Log file created with 2 entries")
# READ to show current state
with open(filename, 'r', encoding='utf-8') as f:
print(f"\nCurrent log:\n{f.read()}")
# APPEND MODE: Add more entries without deleting
print("=== Appending new entries ===")
with open(filename, 'a', encoding='utf-8') as f:
f.write("2025-11-09 10:15 - Data processing started\n")
f.write("2025-11-09 10:30 - Data processing complete\n")
f.write("2025-11-09 10:35 - Program ended\n")
print("Added 3 new entries")
# READ final state
print("\nFinal log:")
with open(filename, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
print(f" {i}. {line.strip()}")
Output:
=== Creating log file ===
Log file created with 2 entries
Current log:
2025-11-09 10:00 - Program started
2025-11-09 10:05 - User login
=== Appending new entries ===
Added 3 new entries
Final log:
1. 2025-11-09 10:00 - Program started
2. 2025-11-09 10:05 - User login
3. 2025-11-09 10:15 - Data processing started
4. 2025-11-09 10:30 - Data processing complete
5. 2025-11-09 10:35 - Program ended
Validation Steps:
- ✓ Write mode creates file; append mode adds to existing content
- ✓ File grows correctly with each append operation
- ✓ All entries are preserved (not overwritten)
- ✓ Perfect pattern for application logs
Code Example 2.4: Comprehensive Error Handling for File Operations
Specification Reference: Show proper exception handling for common file errors AI Prompt Used: "Write a function that reads a file with proper error handling for different failure cases"
def safe_read_file(filename: str) -> str | None:
"""
Read file with comprehensive error handling.
Returns:
File contents as string, or None if error occurs
"""
try:
with open(filename, 'r', encoding='utf-8') as f:
content: str = f.read()
return content
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
print(" Make sure the file exists in the current directory.")
return None
except PermissionError:
print(f"Error: No permission to read '{filename}'.")
print(" Check file permissions (you may need admin access).")
return None
except IOError as e:
print(f"Error reading file: {e}")
print(" This could be a disk error or encoding issue.")
return None
def safe_write_file(filename: str, content: str) -> bool:
"""
Write content to file with error handling.
Returns:
True if successful, False if error occurred
"""
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Successfully wrote to '{filename}'")
return True
except PermissionError:
print(f"Error: No permission to write '{filename}'.")
return False
except IOError as e:
print(f"Error writing file: {e}")
return False
# Test reading non-existent file
print("=== Attempting to read missing file ===")
result: str | None = safe_read_file("nonexistent.txt")
if result:
print(f"Content: {result}")
else:
print("Read failed\n")
# Test writing and reading back
print("=== Write and read test ===")
test_content: str = "Hello from Python!\nThis file was created safely.\n"
success: bool = safe_write_file("test.txt", test_content)
if success:
result = safe_read_file("test.txt")
if result:
print(f"Verified content:\n{result}")
Output:
=== Attempting to read missing file ===
Error: File 'nonexistent.txt' not found.
Make sure the file exists in the current directory.
Read failed
=== Write and read test ===
Successfully wrote to 'test.txt'
Verified content:
Hello from Python!
This file was created safely.
Validation Steps:
- ✓ Catches FileNotFoundError with helpful message
- ✓ Catches PermissionError separately with different guidance
- ✓ Catches generic IOError for other issues
- ✓ Type hints (
str | None,-> bool) make behavior clear - ✓ Reusable functions work across different filenames
- ✓ Returns meaningful data indicating success/failure
Practice Exercises
Exercise 1: Create, Write, and Read Files
Write a program that:
- Creates a file called
inventory.txt - Writes the names of 5 products with quantities
- Reads the file back and displays each line with a line number
Example Output:
Created inventory.txt with 5 products
Inventory:
1. Apples - 25 units
2. Bananas - 18 units
3. Oranges - 12 units
4. Grapes - 8 units
5. Strawberries - 15 units
Acceptance Criteria:
- File contains exactly 5 lines
- Each line has product name and quantity
- Output shows line numbers with proper formatting
- Uses context managers for all file operations
Exercise 2: Append Mode and Log Files
Write a program that:
- Creates a
session.logfile with one initial entry - Appends 3 more entries with timestamps
- Reads and displays the complete log
Example Output:
Log file created
2025-11-09 09:00 - Session started
Log updated with 3 new entries
Complete log:
2025-11-09 09:00 - Session started
2025-11-09 09:15 - User action 1
2025-11-09 09:30 - User action 2
2025-11-09 09:45 - Session ended
Acceptance Criteria:
- Initial write creates file
- Append operations add to file without deleting
- All entries displayed in order
- Works correctly on first run (file doesn't exist yet) and subsequent runs
Exercise 3: Error Handling for File Operations
Write a program that:
- Attempts to read a file that might not exist
- Handles FileNotFoundError by creating the file with default content
- Reads the file and displays it
Example Output:
File 'config.txt' not found. Creating with defaults...
Default config created
Current config:
debug=false
timeout=30
retries=3
Acceptance Criteria:
- Catches FileNotFoundError
- Creates new file with default content
- Reads and displays file on second try
- Uses appropriate try/except blocks
Try With AI
Prompt 1: Remember/Understand
Ask your AI: "What is a context manager? Why do we use the with statement when opening files?"
Expected Outcome: You'll understand that context managers automatically clean up resources, preventing file corruption and resource leaks.
Prompt 2: Apply
Ask your AI: "Write a program that reads a file containing CSV data (each line has name,age,city), parses it, and writes a formatted report to a new file."
Expected Outcome: You'll see file reading, string parsing, and writing combined in a practical workflow.
Prompt 3: Analyze
Ask your AI: "Compare reading a file with read() vs readlines(). When would you use each? What are the memory implications for large files?"
Expected Outcome: You'll understand performance tradeoffs and method selection criteria based on file size and use case.
Prompt 4: Synthesize/Create
Ask your AI: "Design a file backup program that reads the original file, makes modifications (e.g., removing blank lines), and writes to a backup file. Include error handling for missing files and write failures. What's the overall structure?"
Expected Outcome: You'll see how file I/O integrates with data processing and error management at application level, preparing you for Lesson 5 capstone.