Lesson 6

Files

Home › Courses › Python › Lesson 6
🐍 Lesson 6 · 15 minutes

Files.
Persist your data.

Every variable you create lives in memory and disappears when the program ends. Files don't. Reading a config, saving user data, processing a CSV, appending to a log — the moment your code touches the disk, it stops being a script and starts being a tool. This lesson covers the exact patterns you'll use for every file operation in Python.

~15 minutes Beginner → Intermediate Python 3.x
Python path Lesson 5 Read Write with Paths JSON Challenge

To read a file in Python: with open("file.txt", "r") as f: content = f.read(). To write: with open("file.txt", "w") as f: f.write("text"). Use mode "a" to append instead of overwrite. Always use with — it closes the file automatically, even if an error occurs. For JSON, use json.load() / json.dump(). For CSV, use the csv module.

Before this lesson: you should be comfortable with dicts and functions. If not, read Lesson 5 and Lesson 4 first.

📖 Reading files

Four patterns — pick the one that matches your file size.

Python
# 1. Read the whole file as a single string
with open("notes.txt", "r") as f:
content = f.read()
print(content)

# 2. Read all lines into a list
with open("notes.txt", "r") as f:
lines = f.readlines()
# lines is like ['line 1\n', 'line 2\n', 'line 3\n']

# 3. Loop line by line (best for large files)
with open("notes.txt", "r") as f:
for line in f:
print(line.strip()) # .strip() removes trailing newline

# 4. Read a specific number of characters
with open("notes.txt", "r") as f:
first_100 = f.read(100)

f.read()

Returns the entire file as one string. Simple but uses memory proportional to file size. Fine for small files, bad for 1GB logs.

f.readlines()

Returns a list of lines, each ending in \n (except maybe the last). Fine for small-to-medium files. Use .strip() on each line to clean it.

for line in f:

The idiomatic way to read line by line. Uses constant memory regardless of file size. This is what you should use for anything but tiny files.

Encoding

Python 3 defaults to UTF-8 on most systems. For guaranteed behavior, specify: open("f.txt", "r", encoding="utf-8"). This avoids errors with non-ASCII characters.

✍️ Writing files

Three modes: overwrite, append, exclusive-create.

Python
# Mode "w" — OVERWRITE (truncates the file first)
with open("output.txt", "w") as f:
f.write("Hello, file!\n")
f.write("Second line\n")
# Any previous content in output.txt is gone.

# Mode "a" — APPEND (adds to the end, keeps existing content)
with open("log.txt", "a") as f:
f.write("New log entry\n")
# Existing content preserved.

# Mode "x" — EXCLUSIVE CREATE (fails if file exists)
with open("newfile.txt", "x") as f:
f.write("Created fresh\n")
# Raises FileExistsError if file already exists.

# Write multiple lines at once
lines = ["Alice\n", "Bob\n", "Charlie\n"]
with open("names.txt", "w") as f:
f.writelines(lines)

# Common idiom: write lines from a loop
with open("squares.txt", "w") as f:
for n in range(10):
f.write(f"{n} squared = {n*n}\n")

Mode "w" destroys existing files. If output.txt had 10,000 lines of data and you open it with "w", those 10,000 lines are gone before your first write() call. If you want to add data, use "a". If you want to overwrite, mean it. There's no undo — the OS doesn't save a backup.

Always add \n to your strings when writing. f.write("Hello") followed by f.write("World") produces "HelloWorld" on one line, not two lines. write() doesn't add line breaks — you do. This trips up everyone coming from print(), which adds a newline automatically.

🛡️ The with statement — why it's mandatory

The single most important rule of file handling.

The with statement guarantees the file is closed when the block exits — even if an exception is raised, even if you return early, even if the program crashes inside the block. Without with, you must call f.close() manually, and if anything goes wrong before that line, the file stays open. On Windows, an open file can't be deleted or renamed. On any OS, open files leak resources.

Python
# BAD — the file stays open if an error occurs
f = open("data.txt", "r")
content = f.read() # what if this fails?
f.close() # never reached

# GOOD — file closed automatically, even on error
with open("data.txt", "r") as f:
content = f.read()
# file is closed here, guaranteed

# The with statement is equivalent to try/finally
f = open("data.txt", "r")
try:
content = f.read()
finally:
f.close()

# Multiple files in one with
with open("input.txt") as src, open("output.txt", "w") as dst:
for line in src:
dst.write(line.upper())

You should never write f = open(...) without with. If you see it in a tutorial or in your own code, treat it as a bug. The only valid exception is when you intentionally need the file handle to outlive the current block — which is rare and usually wrong. Use with. Always.

📁 Paths — where files actually live

Relative vs absolute, forward vs backslash, and the pathlib module.

When you write open("data.txt"), Python looks for data.txt in the current working directory — the folder you were in when you ran the script, not the folder the script lives in. This distinction confuses everyone at some point.

Python
import os
from pathlib import Path

# Relative path — from current working directory
with open("data.txt") as f:
...

# Same file, using subfolder
with open("data/notes.txt") as f:
...

# Check current working directory
print(os.getcwd()) # /Users/you/projects/my-app

# Absolute path — works from anywhere
with open("/Users/you/data.txt") as f:
...

# Modern way — pathlib (Python 3.4+)
path = Path("data") / "notes.txt" # data/notes.txt, works on Windows too
with path.open() as f:
content = f.read()

# Useful pathlib operations
p = Path("data.txt")
print(p.exists()) # True or False
print(p.name) # "data.txt"
print(p.suffix) # ".txt"
print(p.stem) # "data"
print(p.parent) # Path('.') — the folder containing it
print(p.resolve()) # absolute path

# Create a directory if it doesn't exist
Path("output").mkdir(exist_ok=True)

Relative path

Starts from wherever the script was run. "data.txt" means "in the current folder." Simple, portable, but breaks if run from a different directory.

Absolute path

Starts from the filesystem root. "/home/user/file.txt" or "C:\Users\me\file.txt". Works from anywhere but is machine-specific.

Forward vs backslash

Python accepts forward slashes (/) on all platforms, including Windows. Use them. Backslashes in strings need escaping: "C:\Users\me" or raw strings r"C:\Users\me".

pathlib

Modern, cross-platform path handling. Path("a") / "b" joins paths correctly. Use it for anything complex — string concatenation of paths is a bug waiting to happen.

🗂️ JSON — the format of the web

Every API response, every config file, every saved state.

JSON is how data moves between programs. Every web API returns it. Every config file uses it. Every language parses it. Python has built-in support — json.load() reads from a file, json.dump() writes to a file. The result is always a Python dict (or list, depending on the JSON structure).

Python
import json

# WRITE JSON to a file
user = {
"name": "Alice",
"age": 30,
"hobbies": ["reading", "hiking"],
}
with open("user.json", "w") as f:
json.dump(user, f, indent=2)
# Creates a human-readable JSON file.

# READ JSON from a file
with open("user.json") as f:
loaded = json.load(f)
print(loaded["name"]) # Alice
print(loaded["hobbies"][0]) # reading

# CONVERT to/from strings (not files)
json_string = json.dumps(user) # dict → string
back_to_dict = json.loads(json_string) # string → dict

# Handling missing/invalid JSON
try:
with open("config.json") as f:
config = json.load(f)
except FileNotFoundError:
config = {} # default when file is missing
except json.JSONDecodeError:
config = {} # default when file is corrupt

# Update and save back
config["last_run"] = "2026-10-23"
with open("config.json", "w") as f:
json.dump(config, f, indent=2)

Always use indent=2 when writing JSON meant for humans. json.dump(data, f) produces one unreadable line. json.dump(data, f, indent=2) produces the nicely formatted JSON you see in API docs and config files. For machine-to-machine files (data caches, network payloads), skip the indent — it saves bytes. For anything a human will read, use it.

JSON keys are always strings. If you have a dict with integer keys like {1: "one", 2: "two"} and call json.dumps(), they become strings: {"1": "one", "2": "two"}. When you load them back, the keys are strings, not ints. This is one of the most confusing JSON round-trip bugs.

💻 A complete example

A mini to-do app that persists data to disk.

Python
# todo.py — persistent to-do list
import json

FILE = "todos.json"

def load_todos():
"""Load todos from disk. Return empty list if file missing."""
try:
with open(FILE, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []

def save_todos(todos):
"""Write todos to disk."""
with open(FILE, "w", encoding="utf-8") as f:
json.dump(todos, f, indent=2)

def add_todo(todos, text):
todos.append({"text": text, "done": False})
save_todos(todos)

def mark_done(todos, index):
if 0 <= index < len(todos):
todos[index]["done"] = True
save_todos(todos)

def show_todos(todos):
for i, todo in enumerate(todos):
mark = "✓" if todo["done"] else " "
print(f"[{mark}] {i}. {todo['text']}")

# Use it
todos = load_todos()
add_todo(todos, "Learn Python files")
add_todo(todos, "Write a to-do app")
add_todo(todos, "Ship it")
mark_done(todos, 0)
show_todos(todos)

# Output:
# [✓] 0. Learn Python files
# [ ] 1. Write a to-do app
# [ ] 2. Ship it

# Run again — data persists between runs.

Notice the pattern: load → modify → save. Every persistent app follows this shape. You don't keep the file open and mutate it in place — you read it all into memory, change it, write it all back. This is fast, safe, and how 95% of real applications work with small data. For very large data, use a database.

🏆 Challenge

Three tasks. Each builds on the one before it.

Task 1 — Line counter

Write a function count_lines(filename) that returns the number of lines in a text file. Use a for line in f: loop. Handle FileNotFoundError by returning -1.

Test: create a file with 5 lines, verify count_lines("test.txt") == 5. Then test with a non-existent file: count_lines("missing.txt") == -1.

Task 2 — Word frequency to file

Read a text file, count word frequencies using the .get(key, 0) + 1 pattern (from Lesson 5), and write the top 10 words and their counts to a new file, one per line.

Format: word: count. Hint: use sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:10].

Task 3 — Config manager

Build a tiny config system:

1. load_config(path) — reads a JSON file, returns dict, empty dict if missing.
2. save_config(path, config) — writes the dict to JSON with indent=2.
3. set_config(path, key, value) — loads, sets the key, saves. Returns the updated dict.
4. Test: set_config("config.json", "theme", "dark"), then set_config("config.json", "lang", "en"). Verify both keys exist after reloading.

⚠️ Common mistakes

The eight file errors that catch every beginner.

1. Not using with

f = open(...) without with — if an error occurs before f.close(), the file stays open. On Windows, it can't be deleted. Always use with open(...) as f:.

2. Overwriting when you meant to append

Opened with "w" when you meant "a". The existing file content is destroyed the moment you open it. There's no undo. Check your mode before you click run on anything that writes files.

3. Forgetting the newline in write()

f.write("a") then f.write("b") produces "ab", not two lines. Add \n to each string: f.write("a\n"). Unlike print(), write() doesn't add line breaks.

4. FileNotFoundError on a relative path

open("data.txt") looks in the current working directory, not the script folder. If the script was run from a different folder, the file "isn't there." Fix: use Path(file).parent / "data.txt" for paths relative to the script.

5. Not closing the file after reading

If you use open() without with, Python keeps the file handle open until garbage collection — which might not happen until the program exits. Files stay locked, resources leak. Use with.

6. Reading a binary file in text mode

open("image.jpg", "r") fails with UnicodeDecodeError — you can't decode JPEG bytes as text. Fix: open in binary mode: open("image.jpg", "rb").

7. Corrupt JSON after a write

Write JSON with json.dump(), not f.write(str(data)). str(dict) uses single quotes and isn't valid JSON. The json module guarantees round-trip compatibility with any JSON parser.

8. Assuming an encoding

On Windows, Python defaults to the system encoding (often cp1252). On macOS/Linux, it's typically UTF-8. Same script, different behavior. Fix: always specify encoding="utf-8" explicitly in your open() calls.

➡️ What's next

One lesson left in the beginner tier — error handling.

❓ Frequently asked questions

How do I read a file in Python?

Use the with statement: with open('file.txt', 'r') as f: content = f.read(). This opens the file, reads everything into a string, and closes it automatically when the with block exits. For large files, iterate line by line: for line in f: process(line). For a list of lines: f.readlines(). Always use with — without it, the file stays open and can be locked or leak file handles.

How do I write to a file in Python?

Open with mode 'w' for overwrite or 'a' for append, then call f.write('text'): with open('file.txt', 'w') as f: f.write('Hello'). Mode 'w' truncates the file first — any existing content is gone. Mode 'a' adds to the end. Use 'w' when you're creating a fresh file, 'a' when you're adding to an existing log or data file.

What is the difference between 'w' and 'a' file modes?

'w' (write) truncates the file to zero bytes before writing — any existing content is destroyed. 'a' (append) keeps existing content and adds new data at the end. For logs, append. For output files you regenerate each run, write. There's also 'x' (exclusive create) which fails if the file exists, useful for preventing accidental overwrites.

Why do I need the 'with' statement for files?

The with statement ensures the file is closed properly, even if an error occurs inside the block. Without it, you must call f.close() manually — and if an exception happens before that line, the file stays open and locked. On Windows, an open file can't be deleted or renamed. Use with — always, no exceptions.

How do I read a JSON file in Python?

Import the json module, then: import json; with open('data.json') as f: data = json.load(f). The result is a Python dict (or list, depending on the JSON). To write JSON: with open('out.json', 'w') as f: json.dump(data, f, indent=2). Use json.dumps() to get a string, json.dump() to write directly to a file. Use indent=2 for human-readable output.

How do I read a CSV file in Python?

Use the csv module: import csv; with open('data.csv') as f: reader = csv.reader(f); for row in reader: print(row). Each row is a list of strings. Use csv.DictReader for dict-based rows with header keys: reader = csv.DictReader(f); for row in reader: print(row['name']). Writing works the same way with csv.writer or csv.DictWriter. Always open CSV files with newline='' to handle line endings correctly on all platforms.

What is FileNotFoundError and how do I avoid it?

FileNotFoundError is raised when you open a file that doesn't exist. To avoid it: check with os.path.exists('file.txt') first, or wrap in try/except FileNotFoundError. For reading files that might not exist, use a default: try: with open(path) as f: return f.read() except FileNotFoundError: return ''. Never assume a file exists — always handle the missing case.

What is the difference between text and binary mode?

Text mode ('r', 'w', 'a') reads/writes strings, handles encoding (UTF-8 by default in Python 3), and translates line endings. Binary mode ('rb', 'wb', 'ab') reads/writes bytes, no encoding, no translation. Use text mode for human-readable content, binary mode for images, PDFs, audio, or any non-text file. Reading a binary file in text mode or vice versa corrupts the data.

🌐 External resources

← Back to Python track
You're in the Python track · Lesson 6