Skip to main content

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:

  1. Automatic cleanup: The with statement guarantees the file closes, even if an error occurs
  2. Cleaner syntax: Less boilerplate code than manual open/close
  3. 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 with statement and use file = open(filename) then try to read it. Why is with safer?"

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:

ModeNameBehaviorFile State
'r'ReadRead existing fileFile must exist; not modified
'w'WriteCreate new or overwriteExisting content deleted; creates if doesn't exist
'a'AppendAdd to end of fileExisting content preserved; creates if doesn't exist
'r+'Read + WriteRead and modify in placeFile 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 (with statement) ensures file is closed
  • ✓ Exception handling catches both missing files and I/O errors
  • ✓ Returns meaningful data (str | None type 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:

  1. Creates a file called inventory.txt
  2. Writes the names of 5 products with quantities
  3. 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:

  1. Creates a session.log file with one initial entry
  2. Appends 3 more entries with timestamps
  3. 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:

  1. Attempts to read a file that might not exist
  2. Handles FileNotFoundError by creating the file with default content
  3. 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.