Skip to content

Reading and Writing Files

💡
Before you start

You need Python installed, and to know how to run a .py file. If python3 --version in a terminal prints a version number, you are ready. If it does not — or if you have never opened a terminal — do Getting Started with Python first; it installs Python and runs your first program, and takes about fifteen minutes. Nothing else is needed: no account, no payment, no extra software.

Opening and Closing Files

Python uses the built-in open() function to work with files. The most important rule: always close files when you're done with them.

# Basic pattern (not recommended)
file = open("example.txt", "r")
content = file.read()
file.close()

# Better: use 'with' statement (auto-closes)
with open("example.txt", "r") as file:
    content = file.read()
# File is automatically closed here
⚠️
Always use the with statement

The with statement guarantees the file is closed even if an error occurs. Forgetting to close files can lead to data loss and resource leaks.

File Modes

The second argument to open() specifies how the file should be opened:

# Read modes
"r"      # Read (default) — file must exist
"r+"     # Read and write — file must exist

# Write modes
"w"      # Write — creates file or OVERWRITES existing
"a"      # Append — creates file or adds to end
"x"      # Exclusive create — fails if file exists

# Binary modes (add 'b')
"rb"     # Read binary (images, PDFs, etc.)
"wb"     # Write binary
⚠️
"w" mode erases everything!

Opening a file with "w" immediately deletes all existing content. Use "a" to add to a file without losing data.

Reading Files

Read Entire File

with open("notes.txt", "r") as file:
    content = file.read()
    print(content)

Read Line by Line

# Read all lines into a list
with open("notes.txt", "r") as file:
    lines = file.readlines()
    for line in lines:
        print(line.strip())     # strip() removes trailing newline

# Memory-efficient: iterate directly
with open("notes.txt", "r") as file:
    for line in file:
        print(line.strip())
💡
For large files, iterate directly

file.read() loads the entire file into memory. For large files, iterate line by line with for line in file: — this reads only one line at a time.

Read Specific Amount

with open("notes.txt", "r") as file:
    first_100 = file.read(100)     # Read first 100 characters
    next_line = file.readline()    # Read next line from current position

Writing Files

Write Text

# Create or overwrite a file
with open("output.txt", "w") as file:
    file.write("First line\n")
    file.write("Second line\n")

# Write multiple lines at once
lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open("output.txt", "w") as file:
    file.writelines(lines)

Append to a File

# Add to existing file without erasing
with open("log.txt", "a") as file:
    file.write("New log entry\n")

Write with print()

with open("output.txt", "w") as file:
    print("Hello, file!", file=file)
    print(f"The answer is {42}", file=file)

Working with File Paths

import os
from pathlib import Path

# os.path (traditional)
print(os.path.exists("myfile.txt"))          # True/False
print(os.path.isfile("myfile.txt"))          # True if it's a file
print(os.path.isdir("myfolder"))             # True if it's a directory
print(os.path.join("folder", "file.txt"))    # folder/file.txt

# pathlib (modern, recommended)
p = Path("folder") / "subfolder" / "file.txt"
print(p)                     # folder/subfolder/file.txt
print(p.exists())            # True/False
print(p.suffix)              # .txt
print(p.stem)                # file
print(p.parent)              # folder/subfolder

Practical Examples

Log Writer

from datetime import datetime

def log(message, filename="app.log"):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(filename, "a") as file:
        file.write(f"[{timestamp}] {message}\n")

log("Application started")
log("User logged in")
log("Error: connection timeout")

Simple CSV Reader

# Reading CSV manually
with open("data.csv", "r") as file:
    header = file.readline().strip().split(",")
    print(f"Columns: {header}")

    for line in file:
        values = line.strip().split(",")
        row = dict(zip(header, values))
        print(row)

# Better: use the csv module
import csv

with open("data.csv", "r") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(row["name"], row["age"])

Word Counter

def count_words(filename):
    with open(filename, "r") as file:
        text = file.read()

    words = text.split()
    lines = text.count("\n") + 1
    chars = len(text)

    print(f"File: {filename}")
    print(f"  Lines: {lines}")
    print(f"  Words: {len(words)}")
    print(f"  Characters: {chars}")

count_words("example.txt")

Checking If a File Exists

from pathlib import Path

filepath = Path("config.txt")

if filepath.exists():
    content = filepath.read_text()
    print("File found!")
else:
    print("File not found, creating it...")
    filepath.write_text("default config")

Now Do It Yourself: Five Steps

Reading and writing files is how a program remembers anything after it stops. You will create a file, read it back two different ways, add to it, and meet the one mistake that silently destroys data. Every output below is exactly what Python printed.

1
Write a file from Python

Go: open a terminal, run cd ~, and create a file called notes.py in your editor.

Do: type these two lines and run python3 notes.py.

with open("notes.txt", "w") as f:
    f.write("first line\nsecond line\n")

You should see: no output at all — but a new file notes.txt now sits beside your script. Check with cat notes.txt (or type notes.txt on Windows): two lines. The \n is a newline character; without it everything would run together on one line.

If not: if you cannot find the file, it was created wherever the terminal was, not where the script lives. Run pwd to see where that is. The with is what guarantees the file is closed and the text actually flushed to disk — open a file without it and your text can still be sitting in memory when the program ends.

2
Read the whole thing back

Go: same file, replace the contents.

Do: type this one line and run it.

print(open("notes.txt").read(), end="")

You should see: first line and second line. read() hands back the entire file as one string, newlines included — which is why end="" is there, to stop print adding a third blank line of its own.

If not: FileNotFoundError: [Errno 2] No such file or directory: 'notes.txt' means step 1 has not run, or ran somewhere else. The message names the file it looked for; the folder it looked in is wherever your terminal is.

3
Read it one line at a time instead

Go: same file, replace the contents.

Do: type these three lines and run it.

with open("notes.txt") as f:
    for line in f:
        print("[" + line.strip() + "]")

You should see: [first line] and [second line]. The brackets are there to prove the point: each line arrives with its newline still attached, and strip() is what removes it. Looping over the file this way reads one line at a time, so it works on a file far too big to fit in memory.

If not: if you see a blank line between each result, you left out strip() — the line's own newline plus print's newline makes two. That is the single most common surprise when reading files.

4
Add to a file without destroying it

Go: same file, replace the contents.

Do: type these three lines and run it. Note the "a".

with open("notes.txt", "a") as f:
    f.write("third line\n")
print(open("notes.txt").read(), end="")

You should see: all three lines. "a" means append — write to the end and keep what is already there.

If not: if only third line appears, you used "w" instead of "a". Read the next step before you do that to anything you care about.

5
See exactly how "w" destroys a file — on a file you do not mind losing

Go: same file, replace the contents.

Do: type these two lines and run it.

open("notes.txt", "w").close()
print("after opening with w, length is", len(open("notes.txt").read()))

You should see: after opening with w, length is 0. Nothing was written — merely opening with "w" emptied the file, instantly and with no warning or confirmation.

If not: if the length is not zero, you used "a" or "r". This is the most destructive everyday mistake in file handling, and it is why you read first and write second, never the other way round. Before running any script that writes to a file that matters, copy it: cp notes.txt notes.txt.bak.

🎉
Check yourself before moving on

Without scrolling up: you want to add a line to an existing log file. Which mode, and what happens if you pick the other one? Answer: "a". Picking "w" empties the file the instant it opens, before you write a single character.

Now do it without the page: write a script that reads notes.txt, counts the lines, and appends a final line saying how many there were. Read fully first, then append — steps 3 and 4 joined up.

Summary

  • Always use with open(...) as file: to automatically close files
  • File modes: "r" (read), "w" (write/overwrite), "a" (append), "x" (create new)
  • read() loads entire file; iterate with for line in file: for large files
  • write() and writelines() write to files; remember \n for newlines
  • Use pathlib.Path for modern, cross-platform file path handling
  • The csv module handles CSV files properly (handles quoting, escaping)
🎉
File I/O unlocked!

Your programs can now persist data to disk and read external data. Next up: error handling — making your programs robust when things go wrong.