π Lesson 8.1: Reading and Writing Files
Every program you've written so far has one limitation: when it stops running, all your data disappears. Variables live in memory, and memory is wiped clean when the process ends. Files solve this β they let your program save data to disk so it survives restarts, share data with other programs, and process information that's too large to type in by hand. This lesson teaches you how to read from and write to files in all three languages.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Understand file paths β absolute vs. relative, and cross-platform differences
- Read text files β load an entire file or read it line by line
- Write text files β create new files and overwrite existing ones
- Append to files β add data without erasing what's already there
- Use safe file handling patterns β context managers, try/finally, and using blocks
- Work with CSV data at a basic level
- Handle file errors gracefully β missing files, permission issues, encoding problems
Estimated Time: 60 minutes
Project: Build a simple note-taking app that saves to a file
π In This Lesson
Why Files Matter
Programs need files for three fundamental reasons:
Save data between
program runs"] --> B["SHARE
Exchange data with
other programs"] B --> C["PROCESS
Handle data too large
to type manually"] style A fill:#3b82f6,color:#fff style B fill:#6366f1,color:#fff style C fill:#22c55e,color:#fff
Think about the programs you use every day: a text editor saves your documents, a game saves your progress, a spreadsheet saves your data. Without files, you'd lose everything the moment you closed the application. Even web applications ultimately store their data in files β databases are just sophisticated file management systems.
π‘ The Big Picture
File I/O (input/output) is one of the most common operations in real-world programming. Configuration files, log files, data exports, reports, user uploads β files are everywhere. The patterns you learn here apply whether you're reading a 10-line text file or processing a 10-gigabyte dataset.
File Paths
Before you can read or write a file, you need to tell the computer where it is. That's what a file path does. There are two kinds:
| Type | Description | Example (Windows) | Example (Mac/Linux) |
|---|---|---|---|
| Absolute | Full path from the root of the filesystem | C:\Users\ray\data.txt |
/home/ray/data.txt |
| Relative | Path from the current working directory | data\scores.txt |
data/scores.txt |
import os
from pathlib import Path # Modern Python β recommended!
# --- Absolute vs Relative ---
absolute = "/home/ray/projects/data.txt"
relative = "data/scores.txt" # Relative to where you run the script
# --- The pathlib way (modern Python 3.4+) ---
# Build paths that work on ANY operating system:
data_dir = Path("data")
scores_file = data_dir / "scores.txt" # Uses / operator!
print(scores_file) # data/scores.txt (or data\scores.txt on Windows)
# Useful Path methods:
p = Path("data/reports/q4.txt")
print(p.name) # "q4.txt" β filename
print(p.stem) # "q4" β filename without extension
print(p.suffix) # ".txt" β file extension
print(p.parent) # "data/reports" β parent directory
print(p.exists()) # True or False β does the file exist?
print(p.is_file()) # True or False β is it a file (not directory)?
# Get current working directory:
print(Path.cwd()) # e.g., /home/ray/projects
# --- The os.path way (older but still common) ---
full_path = os.path.join("data", "scores.txt") # Cross-platform join
print(os.path.exists(full_path)) # True/False
print(os.path.basename(full_path)) # "scores.txt"
print(os.path.dirname(full_path)) # "data"
// Node.js β the 'path' module handles cross-platform paths
const path = require("path");
const fs = require("fs");
// --- Absolute vs Relative ---
let absolute = "/home/ray/projects/data.txt";
let relative = "data/scores.txt"; // Relative to where you run node
// --- Building paths safely ---
// ALWAYS use path.join() β never concatenate strings with / or \
let scoresFile = path.join("data", "scores.txt");
console.log(scoresFile); // "data/scores.txt" (or "data\scores.txt" on Windows)
// Useful path methods:
let p = path.join("data", "reports", "q4.txt");
console.log(path.basename(p)); // "q4.txt"
console.log(path.basename(p, ".txt")); // "q4" (without extension)
console.log(path.extname(p)); // ".txt"
console.log(path.dirname(p)); // "data/reports"
// Check if file exists:
console.log(fs.existsSync(scoresFile)); // true or false
// Get current working directory:
console.log(process.cwd()); // e.g., /home/ray/projects
// Get absolute path from relative:
console.log(path.resolve("data", "scores.txt"));
// β /home/ray/projects/data/scores.txt
// __dirname = directory of the CURRENT script file
// __filename = full path of the CURRENT script file
console.log(__dirname); // /home/ray/projects
console.log(__filename); // /home/ray/projects/app.js
using System.IO;
// --- Absolute vs Relative ---
string absolute = @"C:\Users\ray\projects\data.txt"; // @ = verbatim string
string relative = "data/scores.txt"; // Relative to working directory
// --- Building paths safely ---
// ALWAYS use Path.Combine() β never concatenate with + and slashes
string scoresFile = Path.Combine("data", "scores.txt");
Console.WriteLine(scoresFile); // data\scores.txt (Windows) or data/scores.txt
// Useful Path methods:
string p = Path.Combine("data", "reports", "q4.txt");
Console.WriteLine(Path.GetFileName(p)); // "q4.txt"
Console.WriteLine(Path.GetFileNameWithoutExtension(p)); // "q4"
Console.WriteLine(Path.GetExtension(p)); // ".txt"
Console.WriteLine(Path.GetDirectoryName(p)); // "data/reports"
// Check if file/directory exists:
Console.WriteLine(File.Exists(scoresFile)); // True/False
Console.WriteLine(Directory.Exists("data")); // True/False
// Get current working directory:
Console.WriteLine(Directory.GetCurrentDirectory());
// Get absolute path from relative:
Console.WriteLine(Path.GetFullPath("data/scores.txt"));
// Cross-platform separator:
Console.WriteLine(Path.DirectorySeparatorChar); // \ on Windows, / on Mac/Linux
β οΈ Never Hard-Code Path Separators
Don't write "data\\scores.txt" or "data/scores.txt" with literal slashes. Always use your language's path-building tools (Path() / path.join() / Path.Combine()). This ensures your code works on Windows (\), Mac (/), and Linux (/) without changes.
π Instructor Note: Delivery Guidance
File paths trip up beginners more than almost any other topic. The most common issue: students run their script from a different directory than expected, so relative paths don't resolve. Demo this live β create a file in one folder, cd somewhere else, and show how the relative path breaks. Then show Path.cwd() / process.cwd() / Directory.GetCurrentDirectory() to reveal the current directory. The Windows \ vs. Mac/Linux / difference is also a source of cross-platform bugs β emphasize path.join/Path.Combine. If students are on Windows with WSL, they deal with BOTH path conventions, making this extra relevant.
Reading Text Files
The most common file operation: open a text file and read its contents. Every language follows the same basic flow:
Get a file handle"] --> B["READ
Load the contents"] --> C["CLOSE
Release the file"] style A fill:#3b82f6,color:#fff style B fill:#6366f1,color:#fff style C fill:#22c55e,color:#fff
Read the Entire File at Once
# --- Method 1: with statement (recommended!) ---
# 'with' automatically closes the file when the block ends
with open("data/greeting.txt", "r") as file:
content = file.read() # Read entire file as one string
print(content)
# After the 'with' block, file is automatically closed β no cleanup needed!
# --- Method 2: read into a list of lines ---
with open("data/scores.txt", "r") as file:
lines = file.readlines() # List of strings, one per line
print(lines)
# ['Alice,95\n', 'Bob,87\n', 'Charlie,72\n']
# Note: each line includes the \n newline character!
# Strip the newlines:
with open("data/scores.txt", "r") as file:
lines = [line.strip() for line in file.readlines()]
print(lines)
# ['Alice,95', 'Bob,87', 'Charlie,72']
# --- Method 3: Pathlib (cleanest for small files) ---
from pathlib import Path
content = Path("data/greeting.txt").read_text()
print(content)
const fs = require("fs");
// --- Method 1: Synchronous (blocking) ---
// Simple and predictable β good for scripts and learning
let content = fs.readFileSync("data/greeting.txt", "utf-8");
console.log(content);
// --- Method 2: Read as array of lines ---
let lines = fs.readFileSync("data/scores.txt", "utf-8")
.split("\n") // Split into lines
.filter(line => line); // Remove empty lines
console.log(lines);
// ['Alice,95', 'Bob,87', 'Charlie,72']
// --- Method 3: Asynchronous with callback ---
// Non-blocking β good for servers and performance
fs.readFile("data/greeting.txt", "utf-8", (err, data) => {
if (err) {
console.error("Error reading file:", err.message);
return;
}
console.log(data);
});
// --- Method 4: Async/Await (modern, recommended) ---
const fsPromises = require("fs").promises;
async function readGreeting() {
let content = await fsPromises.readFile("data/greeting.txt", "utf-8");
console.log(content);
}
readGreeting();
using System.IO;
// --- Method 1: Read entire file as one string ---
string content = File.ReadAllText("data/greeting.txt");
Console.WriteLine(content);
// --- Method 2: Read as array of lines ---
string[] lines = File.ReadAllLines("data/scores.txt");
foreach (string line in lines)
Console.WriteLine(line);
// Alice,95
// Bob,87
// Charlie,72
// --- Method 3: Async version (for larger files or UI apps) ---
string asyncContent = await File.ReadAllTextAsync("data/greeting.txt");
// --- Method 4: StreamReader (for very large files) ---
using (StreamReader reader = new StreamReader("data/big_file.txt"))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line); // Process one line at a time
}
} // StreamReader automatically closed here
Read Line by Line (Memory-Efficient)
For large files, reading everything at once can use too much memory. Reading line by line processes the file without loading it all into RAM:
# The file object itself is iterable β loop over it directly!
with open("data/large_log.txt", "r") as file:
for line_number, line in enumerate(file, start=1):
line = line.strip() # Remove trailing \n
if "ERROR" in line:
print(f"Line {line_number}: {line}")
# This reads ONE line at a time β works for gigabyte-sized files!
# The file is never fully loaded into memory.
const fs = require("fs");
const readline = require("readline");
// Create a line reader from a file stream
const fileStream = fs.createReadStream("data/large_log.txt");
const rl = readline.createInterface({ input: fileStream });
let lineNumber = 0;
rl.on("line", (line) => {
lineNumber++;
if (line.includes("ERROR")) {
console.log(`Line ${lineNumber}: ${line}`);
}
});
rl.on("close", () => {
console.log("Done reading file.");
});
// For modern async/await style:
// const rl = readline.createInterface({ input: fileStream });
// for await (const line of rl) { ... }
// StreamReader reads line by line β ideal for large files
int lineNumber = 0;
using (StreamReader reader = new StreamReader("data/large_log.txt"))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
lineNumber++;
if (line.Contains("ERROR"))
Console.WriteLine($"Line {lineNumber}: {line}");
}
}
// Or with the modern File.ReadLines() which is lazy (one at a time):
foreach (string line in File.ReadLines("data/large_log.txt"))
{
if (line.Contains("ERROR"))
Console.WriteLine(line);
}
// File.ReadLines() is lazy β it reads on demand, unlike ReadAllLines()
β When to Use Each Approach
- Read all at once β small files (config, settings, short data files) where you need the entire content
- Read line by line β large files (logs, data exports, CSVs) where you process each line independently
- Rule of thumb: if the file might be larger than ~100 MB, read line by line
π Instructor Note: Delivery Guidance
Before running any of these examples, students need an actual file to read. Have them create a simple data/greeting.txt file first (even just "Hello, World!" in it). A common beginner frustration is getting FileNotFoundError because the file doesn't exist or the path is wrong. Demonstrate creating the file manually, then reading it. For the line-by-line reading, create a larger file (20+ lines) so students can see the difference. JavaScript's async nature is worth a brief callout: readFileSync is fine for learning and scripts, but real Node.js apps use async I/O. Don't get into callbacks vs. promises vs. async/await in detail here β just show the sync version and mention that the async version exists.
Writing Text Files
Writing is the reverse of reading: you open a file, send content to it, and close it. The critical distinction is the file mode:
| Mode | Description | If File Exists | If File Doesn't Exist |
|---|---|---|---|
"w" β Write |
Create or overwrite | β οΈ Erases all content! | Creates new file |
"a" β Append |
Add to the end | Adds after existing content | Creates new file |
"r" β Read |
Read only | Opens for reading | Error! |
"x" β Exclusive |
Create only (fail if exists) | Error! (won't overwrite) | Creates new file |
# --- Write: creates file or OVERWRITES if it exists ---
with open("output/report.txt", "w") as file:
file.write("Sales Report\n")
file.write("============\n\n")
file.write("Q1: $45,000\n")
file.write("Q2: $52,000\n")
# --- Write multiple lines at once ---
lines = ["Alice: 95\n", "Bob: 87\n", "Charlie: 72\n"]
with open("output/grades.txt", "w") as file:
file.writelines(lines) # Note: writelines does NOT add \n automatically!
# --- Pathlib shortcut ---
from pathlib import Path
Path("output/quick.txt").write_text("Hello from Pathlib!\n")
# --- Using print() to write to a file ---
with open("output/log.txt", "w") as file:
print("Starting process...", file=file) # print to file!
print(f"Result: {42}", file=file)
print("Done.", file=file)
# --- DANGER: "w" mode erases existing content! ---
# This is the #1 file-writing mistake:
with open("important_data.txt", "w") as file: # β οΈ Erases everything!
file.write("Oops, old data is gone forever")
const fs = require("fs");
// --- Write: creates file or OVERWRITES if it exists ---
fs.writeFileSync("output/report.txt",
"Sales Report\n" +
"============\n\n" +
"Q1: $45,000\n" +
"Q2: $52,000\n"
);
// --- Write from an array of lines ---
let lines = ["Alice: 95", "Bob: 87", "Charlie: 72"];
fs.writeFileSync("output/grades.txt", lines.join("\n") + "\n");
// --- Async version (recommended for production) ---
const fsPromises = require("fs").promises;
async function writeReport() {
await fsPromises.writeFile("output/report.txt", "Sales Report\n");
console.log("Report written!");
}
writeReport();
// --- DANGER: writeFileSync OVERWRITES by default! ---
fs.writeFileSync("important_data.txt", "Oops, old data is gone!");
// To create a file ONLY if it doesn't exist:
try {
fs.writeFileSync("output/safe.txt", "New content", { flag: "wx" });
} catch (err) {
if (err.code === "EEXIST") {
console.log("File already exists β not overwriting.");
}
}
using System.IO;
// --- Write: creates file or OVERWRITES if it exists ---
File.WriteAllText("output/report.txt",
"Sales Report\n" +
"============\n\n" +
"Q1: $45,000\n" +
"Q2: $52,000\n"
);
// --- Write from an array of lines ---
string[] lines = { "Alice: 95", "Bob: 87", "Charlie: 72" };
File.WriteAllLines("output/grades.txt", lines);
// WriteAllLines automatically adds newlines between entries!
// --- Async version ---
await File.WriteAllTextAsync("output/report.txt", "Sales Report\n");
// --- StreamWriter for more control ---
using (StreamWriter writer = new StreamWriter("output/log.txt"))
{
writer.WriteLine("Starting process..."); // Adds newline
writer.WriteLine($"Result: {42}");
writer.Write("No newline after this"); // No newline
}
// --- DANGER: WriteAllText OVERWRITES by default! ---
File.WriteAllText("important_data.txt", "Oops, old data is gone!");
// To safely avoid overwriting:
if (!File.Exists("output/safe.txt"))
{
File.WriteAllText("output/safe.txt", "New content");
}
else
{
Console.WriteLine("File already exists β not overwriting.");
}
β οΈ The Most Dangerous File Operation
Opening a file in write mode ("w") immediately erases all existing content β even before you write anything new. This is the single most common cause of accidental data loss in programming. If you want to add to a file without erasing it, use append mode (next section). Always double-check your mode before writing to an important file.
Appending to Files
Append mode adds content to the end of a file without touching what's already there. This is how log files, journals, and running records work.
from datetime import datetime
# --- Append mode: "a" ---
# If the file doesn't exist, it creates it.
# If it does exist, new content goes at the END.
def log_event(message):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open("app.log", "a") as file:
file.write(f"[{timestamp}] {message}\n")
log_event("Application started")
log_event("User logged in: alice")
log_event("Processing order #1234")
# app.log now contains:
# [2026-04-13 10:30:01] Application started
# [2026-04-13 10:30:01] User logged in: alice
# [2026-04-13 10:30:01] Processing order #1234
# Run the program again β new entries are ADDED, old ones stay!
const fs = require("fs");
// --- Append with appendFileSync ---
function logEvent(message) {
let timestamp = new Date().toISOString().slice(0, 19).replace("T", " ");
fs.appendFileSync("app.log", `[${timestamp}] ${message}\n`);
}
logEvent("Application started");
logEvent("User logged in: alice");
logEvent("Processing order #1234");
// --- Or use the flag option with writeFileSync ---
fs.writeFileSync("app.log", "Extra line\n", { flag: "a" });
// flag: "a" = append mode
// --- Async version ---
const fsPromises = require("fs").promises;
async function logEventAsync(message) {
let timestamp = new Date().toISOString().slice(0, 19).replace("T", " ");
await fsPromises.appendFile("app.log", `[${timestamp}] ${message}\n`);
}
logEventAsync("Async log entry");
using System.IO;
// --- Append with File.AppendAllText ---
void LogEvent(string message)
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
File.AppendAllText("app.log", $"[{timestamp}] {message}\n");
}
LogEvent("Application started");
LogEvent("User logged in: alice");
LogEvent("Processing order #1234");
// --- Append multiple lines ---
string[] newEntries = { "Entry A", "Entry B", "Entry C" };
File.AppendAllLines("app.log", newEntries); // Each gets a newline
// --- StreamWriter in append mode ---
using (StreamWriter writer = new StreamWriter("app.log", append: true))
{
writer.WriteLine("StreamWriter append entry");
}
// --- Async version ---
await File.AppendAllTextAsync("app.log", "Async entry\n");
π‘ Write vs. Append β Quick Decision
- Use Write (
"w") when you want a fresh file every time β generating a report, exporting data, saving the current state - Use Append (
"a") when you want to accumulate data over time β log files, history, journal entries
Safe File Handling
When you open a file, your operating system allocates resources (a "file handle") to track it. If your program crashes or you forget to close the file, those resources leak β and on some systems, the file stays locked. Every language provides a pattern for guaranteed cleanup:
# β UNSAFE: if an error occurs, the file might not close
file = open("data.txt", "r")
content = file.read() # What if this line throws an error?
file.close() # This might never run!
# β
SAFE: 'with' guarantees the file closes, even if an error occurs
with open("data.txt", "r") as file:
content = file.read()
# file.close() is called automatically β even if an exception is thrown!
# Why? The 'with' statement calls __enter__() on entry and
# __exit__() on exit β __exit__() always closes the file.
# β
ALSO SAFE: try/finally (but 'with' is preferred)
file = open("data.txt", "r")
try:
content = file.read()
finally:
file.close() # Runs even if an exception occurs
# Multiple files at once:
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
for line in infile:
outfile.write(line.upper()) # Copy with transformation
const fs = require("fs");
// For readFileSync/writeFileSync, Node.js handles
// open and close internally β no explicit cleanup needed:
let content = fs.readFileSync("data.txt", "utf-8"); // Opens, reads, closes
// But for streams (large files), you manage the lifecycle:
// β UNSAFE: file descriptor might leak on error
const fd = fs.openSync("data.txt", "r");
const buffer = Buffer.alloc(1024);
fs.readSync(fd, buffer); // What if this throws?
fs.closeSync(fd); // Might never run!
// β
SAFE: try/finally ensures cleanup
const fd2 = fs.openSync("data.txt", "r");
try {
const buffer2 = Buffer.alloc(1024);
fs.readSync(fd2, buffer2);
} finally {
fs.closeSync(fd2); // Always runs!
}
// β
BEST: Use high-level APIs that handle cleanup for you
// readFileSync, writeFileSync, appendFileSync β all safe
// For streams, attach error handlers:
const stream = fs.createReadStream("data.txt");
stream.on("error", (err) => {
console.error("Stream error:", err.message);
// Stream closes automatically on error
});
using System.IO;
// β UNSAFE: if an error occurs, reader might not close
StreamReader reader = new StreamReader("data.txt");
string content = reader.ReadToEnd(); // What if this throws?
reader.Close(); // Might never run!
// β
SAFE: 'using' guarantees Dispose() is called (which closes the file)
using (StreamReader safeReader = new StreamReader("data.txt"))
{
string safeContent = safeReader.ReadToEnd();
} // safeReader.Dispose() called automatically β even on exception!
// β
MODERN C# 8+: using declaration (no braces needed)
using StreamReader modernReader = new StreamReader("data.txt");
string modernContent = modernReader.ReadToEnd();
// Disposed when modernReader goes out of scope (end of method/block)
// β
SIMPLEST: Use File.ReadAllText / WriteAllText
// These handle open + read/write + close internally:
string easy = File.ReadAllText("data.txt"); // Safe by design
File.WriteAllText("output.txt", "Safe!"); // Safe by design
// Multiple files:
using StreamReader input = new StreamReader("input.txt");
using StreamWriter output = new StreamWriter("output.txt");
string? line;
while ((line = input.ReadLine()) != null)
{
output.WriteLine(line.ToUpper()); // Copy with transformation
}
The Pattern Across Languages
| Concept | π Python | β‘ JavaScript | π· C# |
|---|---|---|---|
| Auto-cleanup keyword | with |
N/A (use high-level APIs) | using |
| Manual cleanup | try/finally + close() |
try/finally + closeSync() |
try/finally + Dispose() |
| Safest simple API | Path.read_text() |
fs.readFileSync() |
File.ReadAllText() |
π Instructor Note: Delivery Guidance
The key message: always use the safe pattern β with in Python, using in C#, and the high-level APIs in Node.js. The "unsafe" examples exist to show why the safe patterns exist, not as recommended practice. For beginners, the simplest advice is: in Python always use with open(), in C# always use File.ReadAllText()/WriteAllText() for small files or using for streams, and in Node.js always use readFileSync/writeFileSync for scripts. Stream-based approaches can be deferred until students work with larger files.
Working with CSV Files
CSV (Comma-Separated Values) is one of the most common data formats. Every spreadsheet can export CSV, and most data tools can read it. A CSV file is just a text file where values are separated by commas:
name,score,grade
Alice,95,A
Bob,87,B
Charlie,72,C
import csv
# --- Reading CSV ---
with open("students.csv", "r") as file:
reader = csv.reader(file)
header = next(reader) # First row is the header
print(f"Columns: {header}") # ['name', 'score', 'grade']
for row in reader:
name, score, grade = row # Unpack each row
print(f"{name} scored {score} ({grade})")
# --- Reading CSV as dictionaries (recommended!) ---
with open("students.csv", "r") as file:
reader = csv.DictReader(file) # Uses header row as keys
for row in reader:
print(f"{row['name']} scored {row['score']} ({row['grade']})")
# row is {'name': 'Alice', 'score': '95', 'grade': 'A'}
# Note: all values are STRINGS β convert numbers yourself!
# --- Writing CSV ---
students = [
{"name": "Alice", "score": 95, "grade": "A"},
{"name": "Bob", "score": 87, "grade": "B"},
{"name": "Charlie", "score": 72, "grade": "C"},
]
with open("output.csv", "w", newline="") as file:
writer = csv.DictWriter(file, fieldnames=["name", "score", "grade"])
writer.writeheader() # Write the header row
writer.writerows(students) # Write all data rows
const fs = require("fs");
// --- Reading CSV (manual parsing for simple cases) ---
let csvText = fs.readFileSync("students.csv", "utf-8");
let lines = csvText.trim().split("\n");
let header = lines[0].split(","); // ['name', 'score', 'grade']
let students = lines.slice(1).map(line => {
let values = line.split(",");
return {
name: values[0],
score: parseInt(values[1]), // Convert to number!
grade: values[2],
};
});
console.log(students);
// [{name: 'Alice', score: 95, grade: 'A'}, ...]
// --- Writing CSV ---
let outputStudents = [
{ name: "Alice", score: 95, grade: "A" },
{ name: "Bob", score: 87, grade: "B" },
{ name: "Charlie", score: 72, grade: "C" },
];
let csvHeader = "name,score,grade\n";
let csvRows = outputStudents
.map(s => `${s.name},${s.score},${s.grade}`)
.join("\n");
fs.writeFileSync("output.csv", csvHeader + csvRows + "\n");
// β οΈ This simple approach breaks if values contain commas!
// "Smith, Jr.",95,A β splits wrong!
// For real CSV parsing, use a library like 'csv-parse' or 'papaparse':
// npm install csv-parse
using System.IO;
// --- Reading CSV (manual parsing for simple cases) ---
string[] lines = File.ReadAllLines("students.csv");
string[] header = lines[0].Split(","); // ["name", "score", "grade"]
var students = new List<Dictionary<string, string>>();
for (int i = 1; i < lines.Length; i++)
{
string[] values = lines[i].Split(",");
students.Add(new Dictionary<string, string>
{
["name"] = values[0],
["score"] = values[1],
["grade"] = values[2],
});
}
foreach (var student in students)
Console.WriteLine($"{student["name"]} scored {student["score"]}");
// --- Writing CSV ---
var outputLines = new List<string> { "name,score,grade" };
outputLines.Add("Alice,95,A");
outputLines.Add("Bob,87,B");
outputLines.Add("Charlie,72,C");
File.WriteAllLines("output.csv", outputLines);
// β οΈ Manual parsing breaks with commas in values!
// For production CSV handling, use a library like CsvHelper:
// dotnet add package CsvHelper
β οΈ CSV Gotcha: Commas in Values
Simple .split(",") parsing breaks when values themselves contain commas β like "Smith, Jr.". Real CSV uses quotes to wrap values that contain commas. For anything beyond simple data, use a proper CSV library: Python's built-in csv module, Node.js csv-parse or papaparse, or C#'s CsvHelper NuGet package. Python has the best built-in CSV support of the three.
π Instructor Note: Delivery Guidance
CSV is a perfect "real-world" file format for beginners because they can create and inspect CSV files in any spreadsheet app. Have students create a small CSV in a text editor or export one from Google Sheets, then read it with their code. The manual JS/C# parsing is intentionally simple β it works for clean data but fails with edge cases. This sets up the motivation for JSON in the next lesson (lesson 23), which handles nested/complex data much better. Python's csv module is notably better than the manual approaches in JS/C# β highlight this as a Python strength.
Handling File Errors
Files live in the real world, and the real world is messy. Files might not exist, you might not have permission to read them, or the disk might be full. Your code needs to handle these gracefully.
# --- Handle specific file errors ---
try:
with open("missing_file.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("Error: File not found. Check the path and filename.")
except PermissionError:
print("Error: No permission to read this file.")
except UnicodeDecodeError:
print("Error: File is not valid text (might be binary).")
except IsADirectoryError:
print("Error: That's a directory, not a file.")
except OSError as e:
print(f"Error: Unexpected file error β {e}")
# --- Check before opening ---
from pathlib import Path
file_path = Path("data/config.txt")
if not file_path.exists():
print(f"File not found: {file_path}")
elif not file_path.is_file():
print(f"Not a file: {file_path}")
else:
content = file_path.read_text()
print(f"Read {len(content)} characters.")
# --- Create directories if they don't exist ---
output_dir = Path("output/reports/2026")
output_dir.mkdir(parents=True, exist_ok=True) # Creates all needed folders
Path(output_dir / "q1.txt").write_text("Q1 Report")
# --- Specifying encoding (important!) ---
# Default encoding varies by OS! Always be explicit:
with open("data.txt", "r", encoding="utf-8") as file:
content = file.read()
with open("output.txt", "w", encoding="utf-8") as file:
file.write("HΓ©llo WΓΆrld! δ½ ε₯½δΈη")
const fs = require("fs");
const path = require("path");
// --- Handle specific file errors ---
try {
let content = fs.readFileSync("missing_file.txt", "utf-8");
} catch (err) {
switch (err.code) {
case "ENOENT":
console.log("Error: File not found. Check the path.");
break;
case "EACCES":
console.log("Error: No permission to read this file.");
break;
case "EISDIR":
console.log("Error: That's a directory, not a file.");
break;
default:
console.log(`Error: ${err.message}`);
}
}
// --- Check before opening ---
let filePath = "data/config.txt";
if (!fs.existsSync(filePath)) {
console.log(`File not found: ${filePath}`);
} else {
let stats = fs.statSync(filePath);
if (!stats.isFile()) {
console.log(`Not a file: ${filePath}`);
} else {
let content = fs.readFileSync(filePath, "utf-8");
console.log(`Read ${content.length} characters.`);
}
}
// --- Create directories if they don't exist ---
let outputDir = path.join("output", "reports", "2026");
fs.mkdirSync(outputDir, { recursive: true }); // Creates all needed folders
fs.writeFileSync(path.join(outputDir, "q1.txt"), "Q1 Report");
// --- Encoding is explicit in Node.js ---
// Always pass "utf-8" as the second argument to readFileSync
// Without it, you get a raw Buffer instead of a string:
let buffer = fs.readFileSync("data.txt"); // Buffer (raw bytes)
let text = fs.readFileSync("data.txt", "utf-8"); // String (decoded)
using System.IO;
using System.Text;
// --- Handle specific file errors ---
try
{
string content = File.ReadAllText("missing_file.txt");
}
catch (FileNotFoundException)
{
Console.WriteLine("Error: File not found. Check the path.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Error: No permission to read this file.");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("Error: The directory doesn't exist.");
}
catch (IOException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
// --- Check before opening ---
string filePath = "data/config.txt";
if (!File.Exists(filePath))
{
Console.WriteLine($"File not found: {filePath}");
}
else
{
string content = File.ReadAllText(filePath);
Console.WriteLine($"Read {content.Length} characters.");
}
// --- Create directories if they don't exist ---
string outputDir = Path.Combine("output", "reports", "2026");
Directory.CreateDirectory(outputDir); // Creates all needed folders
File.WriteAllText(Path.Combine(outputDir, "q1.txt"), "Q1 Report");
// --- Specifying encoding ---
// C# defaults to UTF-8 for File.ReadAllText, but be explicit:
string utf8Content = File.ReadAllText("data.txt", Encoding.UTF8);
// Writing with encoding:
File.WriteAllText("output.txt", "HΓ©llo WΓΆrld! δ½ ε₯½δΈη", Encoding.UTF8);
β File Error Handling Checklist
- Always handle
FileNotFoundβ the most common file error - Create directories before writing β use
mkdir(parents=True)/mkdirSync({recursive: true})/Directory.CreateDirectory() - Specify encoding explicitly β always use
utf-8unless you have a specific reason not to - Check before or catch after β either verify the file exists before opening, or use try/catch
- Give helpful error messages β "File not found" is better than a raw stack trace for users
Exercises
ποΈ Exercise 1: Word Counter
Objective: Write a program that reads a text file and reports how many lines, words, and characters it contains (like the wc command on Unix).
Steps:
- Create a file called
sample.txtwith 5β10 lines of any text - Read the file and count: total lines, total words, and total characters
- Print the results in a formatted summary
- Handle the case where the file doesn't exist
Expected output:
File: sample.txt
Lines: 7
Words: 42
Characters: 238
π‘ Hints
Words can be counted by splitting each line on whitespace. Characters include spaces and punctuation. Don't count the trailing newline character at the end of each line β strip it first.
β Solution
from pathlib import Path
def word_count(filename):
path = Path(filename)
if not path.exists():
print(f"Error: '{filename}' not found.")
return
lines = 0
words = 0
chars = 0
with open(path, "r", encoding="utf-8") as file:
for line in file:
line = line.rstrip("\n") # Remove trailing newline
lines += 1
words += len(line.split())
chars += len(line)
print(f"File: {filename}")
print(f"Lines: {lines}")
print(f"Words: {words}")
print(f"Characters: {chars}")
word_count("sample.txt")
const fs = require("fs");
function wordCount(filename) {
if (!fs.existsSync(filename)) {
console.log(`Error: '${filename}' not found.`);
return;
}
let content = fs.readFileSync(filename, "utf-8");
let lines = content.trimEnd().split("\n");
let totalWords = 0;
let totalChars = 0;
for (let line of lines) {
totalWords += line.split(/\s+/).filter(w => w).length;
totalChars += line.length;
}
console.log(`File: ${filename}`);
console.log(`Lines: ${lines.length}`);
console.log(`Words: ${totalWords}`);
console.log(`Characters: ${totalChars}`);
}
wordCount("sample.txt");
void WordCount(string filename)
{
if (!File.Exists(filename))
{
Console.WriteLine($"Error: '{filename}' not found.");
return;
}
string[] lines = File.ReadAllLines(filename);
int totalWords = 0;
int totalChars = 0;
foreach (string line in lines)
{
totalWords += line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
totalChars += line.Length;
}
Console.WriteLine($"File: {filename}");
Console.WriteLine($"Lines: {lines.Length}");
Console.WriteLine($"Words: {totalWords}");
Console.WriteLine($"Characters: {totalChars}");
}
WordCount("sample.txt");
ποΈ Exercise 2: Simple Note-Taking App
Objective: Build a command-line note-taking app that saves notes to a file. The app should support adding new notes (with timestamps), viewing all notes, and searching notes.
Features:
- Add a note: User types a note, it's saved with a timestamp to
notes.txt - View all notes: Display all saved notes
- Search notes: Find notes containing a keyword
- Notes should persist between program runs (use append mode!)
β Solution
from datetime import datetime
from pathlib import Path
NOTES_FILE = "notes.txt"
def add_note():
note = input("Enter your note: ").strip()
if not note:
print("Empty note β nothing saved.")
return
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
with open(NOTES_FILE, "a", encoding="utf-8") as file:
file.write(f"[{timestamp}] {note}\n")
print("Note saved!")
def view_notes():
path = Path(NOTES_FILE)
if not path.exists() or path.stat().st_size == 0:
print("No notes yet.")
return
print("\n--- All Notes ---")
with open(NOTES_FILE, "r", encoding="utf-8") as file:
for i, line in enumerate(file, 1):
print(f" {i}. {line.strip()}")
print()
def search_notes():
keyword = input("Search for: ").strip().lower()
if not keyword:
print("No keyword entered.")
return
path = Path(NOTES_FILE)
if not path.exists():
print("No notes yet.")
return
print(f"\n--- Notes matching '{keyword}' ---")
found = 0
with open(NOTES_FILE, "r", encoding="utf-8") as file:
for line in file:
if keyword in line.lower():
print(f" β’ {line.strip()}")
found += 1
if found == 0:
print(" No matches found.")
print()
# --- Main loop ---
while True:
print("Notes App: [A]dd | [V]iew | [S]earch | [Q]uit")
choice = input("> ").strip().lower()
if choice == "a":
add_note()
elif choice == "v":
view_notes()
elif choice == "s":
search_notes()
elif choice == "q":
print("Goodbye!")
break
else:
print("Invalid choice. Try A, V, S, or Q.")
const fs = require("fs");
const readline = require("readline");
const NOTES_FILE = "notes.txt";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function prompt(question) {
return new Promise(resolve => rl.question(question, resolve));
}
function addNote(text) {
let timestamp = new Date().toISOString().slice(0, 16).replace("T", " ");
fs.appendFileSync(NOTES_FILE, `[${timestamp}] ${text}\n`);
console.log("Note saved!");
}
function viewNotes() {
if (!fs.existsSync(NOTES_FILE)) {
console.log("No notes yet.");
return;
}
let content = fs.readFileSync(NOTES_FILE, "utf-8").trim();
if (!content) { console.log("No notes yet."); return; }
console.log("\n--- All Notes ---");
content.split("\n").forEach((line, i) => {
console.log(` ${i + 1}. ${line}`);
});
console.log();
}
function searchNotes(keyword) {
if (!fs.existsSync(NOTES_FILE)) {
console.log("No notes yet.");
return;
}
let lines = fs.readFileSync(NOTES_FILE, "utf-8").trim().split("\n");
let matches = lines.filter(l => l.toLowerCase().includes(keyword.toLowerCase()));
console.log(`\n--- Notes matching '${keyword}' ---`);
if (matches.length === 0) {
console.log(" No matches found.");
} else {
matches.forEach(m => console.log(` β’ ${m}`));
}
console.log();
}
async function main() {
while (true) {
let choice = await prompt("Notes App: [A]dd | [V]iew | [S]earch | [Q]uit\n> ");
choice = choice.trim().toLowerCase();
if (choice === "a") {
let note = await prompt("Enter your note: ");
if (note.trim()) addNote(note.trim());
else console.log("Empty note β nothing saved.");
} else if (choice === "v") {
viewNotes();
} else if (choice === "s") {
let keyword = await prompt("Search for: ");
if (keyword.trim()) searchNotes(keyword.trim());
} else if (choice === "q") {
console.log("Goodbye!");
rl.close();
break;
}
}
}
main();
const string NotesFile = "notes.txt";
void AddNote()
{
Console.Write("Enter your note: ");
string? note = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(note))
{
Console.WriteLine("Empty note β nothing saved.");
return;
}
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
File.AppendAllText(NotesFile, $"[{timestamp}] {note}\n");
Console.WriteLine("Note saved!");
}
void ViewNotes()
{
if (!File.Exists(NotesFile) || new FileInfo(NotesFile).Length == 0)
{
Console.WriteLine("No notes yet.");
return;
}
Console.WriteLine("\n--- All Notes ---");
string[] lines = File.ReadAllLines(NotesFile);
for (int i = 0; i < lines.Length; i++)
Console.WriteLine($" {i + 1}. {lines[i]}");
Console.WriteLine();
}
void SearchNotes()
{
Console.Write("Search for: ");
string? keyword = Console.ReadLine()?.Trim().ToLower();
if (string.IsNullOrEmpty(keyword)) return;
if (!File.Exists(NotesFile))
{
Console.WriteLine("No notes yet.");
return;
}
Console.WriteLine($"\n--- Notes matching '{keyword}' ---");
var matches = File.ReadAllLines(NotesFile)
.Where(line => line.ToLower().Contains(keyword))
.ToList();
if (matches.Count == 0)
Console.WriteLine(" No matches found.");
else
matches.ForEach(m => Console.WriteLine($" β’ {m}"));
Console.WriteLine();
}
// Main loop
while (true)
{
Console.WriteLine("Notes App: [A]dd | [V]iew | [S]earch | [Q]uit");
Console.Write("> ");
string? choice = Console.ReadLine()?.Trim().ToLower();
switch (choice)
{
case "a": AddNote(); break;
case "v": ViewNotes(); break;
case "s": SearchNotes(); break;
case "q": Console.WriteLine("Goodbye!"); return;
default: Console.WriteLine("Invalid choice."); break;
}
}
ποΈ Exercise 3: CSV Grade Processor
Objective: Read a CSV file of student grades, calculate the average for each student, assign a letter grade, and write the results to a new CSV file.
Input file (grades_input.csv):
name,test1,test2,test3
Alice,95,88,92
Bob,72,65,78
Charlie,88,91,85
Diana,55,42,61
Expected output file (grades_output.csv):
name,average,letter_grade
Alice,91.7,A
Bob,71.7,C
Charlie,88.0,B
Diana,52.7,F
π‘ Hints
Remember that CSV values are strings β convert test scores to numbers before averaging. Round averages to one decimal place. Use the standard grading scale: A β₯ 90, B β₯ 80, C β₯ 70, D β₯ 60, F below 60.
π Instructor Note: Delivery Guidance
Exercise 1 (Word Counter) is a straightforward read exercise β good for building confidence. Exercise 2 (Note-Taking App) is the star of this lesson β it combines reading, writing, appending, and error handling into a real, useful program. Students should run it, add some notes, quit, restart, and see that their notes persisted. That "aha" moment when data survives a restart is the whole point of this lesson. Exercise 3 (CSV Processor) bridges to the next lesson on JSON by working with structured data. The input CSV should be created by students before they start coding. Consider having them export it from Google Sheets to reinforce the real-world connection.
Summary
π Key Takeaways
- File paths: Use your language's path tools (
Path()/path.join()/Path.Combine()) β never hard-code slashes - Reading files: Read all at once for small files, line by line for large files
- Writing files:
"w"mode erases existing content β be careful! - Appending:
"a"mode adds to the end β use for logs, journals, and running records - Safe handling: Always use
with(Python),using(C#), or high-level APIs (Node.js) to ensure files are closed - CSV files: Simple to parse for clean data; use libraries for real-world data with commas in values
- Error handling: Always handle
FileNotFoundError, create directories before writing, and specifyutf-8encoding
Cross-Language Comparison
| Operation | π Python | β‘ JavaScript (Node.js) | π· C# |
|---|---|---|---|
| Read all text | Path(f).read_text() |
fs.readFileSync(f, "utf-8") |
File.ReadAllText(f) |
| Read all lines | file.readlines() |
content.split("\n") |
File.ReadAllLines(f) |
| Write (overwrite) | open(f, "w") |
fs.writeFileSync(f, data) |
File.WriteAllText(f, data) |
| Append | open(f, "a") |
fs.appendFileSync(f, data) |
File.AppendAllText(f, data) |
| Safe cleanup | with open() as f: |
Built into Sync APIs | using (...) { } |
| File exists? | Path(f).exists() |
fs.existsSync(f) |
File.Exists(f) |
| Join paths | Path("a") / "b" |
path.join("a", "b") |
Path.Combine("a", "b") |
π What's Next?
Now that you can read and write files, you're ready for a more powerful data format. In the next lesson, we'll work with JSON β the universal language for structured data that's used by every API, config file, and data exchange on the web.
π― Quick Check
Question 1: What happens when you open a file in write mode ("w") that already has content?
Question 2: What is the Python with statement used for in file handling?
Question 3: Why should you always use path.join() or Path.Combine() instead of string concatenation for file paths?