All 57 lessons

Lesson 21 of 57 · Files, Errors and Modules

Python File Handling: Reading and Writing Text Files

By Skillancy Editorial Team

Run the code in the free Python Compiler or on your own computer. Lessons that read files use the FreshBasket practice data: download the dataset (ZIP).

Until now every value in your programs came from code you typed. Real analysis starts with data that lives in files: exports from a database, logs from a server, feedback typed by customers. Reading and writing files is the bridge between your program and the outside world, and it is also where many beginner mistakes (forgotten close(), wrong paths, garbled characters) happen.

In this lesson you will learn the safe modern way to work with files: the with statement and the pathlib module. Your practice file is feedback.txt, 40 lines of free-text customer reviews in the form date | order | rating:N | text. By the end you will have parsed it, summarised it, and written a clean report file of your own.

Worked examples

Example 1: Read the whole file

Question: Read the feedback file and count its lines and characters.

with open("feedback.txt", encoding="utf-8") as f:
    text = f.read()

print(len(text), "characters")
print(len(text.splitlines()), "lines")
print(text.splitlines()[0])

Output

4421 characters
40 lines
2025-12-12 | O04344 | rating:3 | Late delivery by almost 50 minutes. Very disappointed. Call me on +91 92699 33648.

How it works: open(name) returns a file object, and with ... as f guarantees the file is closed automatically when the block ends, even if an error occurs. f.read() returns the entire content as one string. Always pass encoding="utf-8" so that text such as ₹ or Hindi names is read the same way on every machine.

Reading the result: The file holds 4,421 characters across 40 lines. Reading everything at once is fine for small files but not for multi-gigabyte logs.

Example 2: Loop over lines

Question: Print the first three reviews and count the low ratings.

low = 0
with open("feedback.txt", encoding="utf-8") as f:
    for i, line in enumerate(f):
        line = line.rstrip("\n")
        if i < 3:
            print(i, line[:70])
        if "rating:1" in line or "rating:2" in line:
            low += 1

print("low-rated:", low)

Output

0 2025-12-12 | O04344 | rating:3 | Late delivery by almost 50 minutes. V
1 2026-01-05 | O00290 | rating:2 | Late delivery by almost 12 minutes. V
2 2025-09-06 | O03999 | rating:3 | Rider could not find my address. Reac
low-rated: 14

How it works: A file object is iterable: looping over it yields one line at a time without loading the whole file, so it works for huge files. Each line still carries its trailing newline, which rstrip("\n") removes. enumerate supplies the line number.

Reading the result: 14 of the lines had a rating of 1 or 2. Looping line by line is the pattern for logs and large exports.

Example 3: readlines and read with limits

Question: What other ways are there to read?

with open("feedback.txt", encoding="utf-8") as f:
    first = f.readline()
    next_two = [f.readline() for _ in range(2)]
    rest = f.readlines()

print(first.strip()[:40])
print(len(next_two), len(rest))

with open("feedback.txt", encoding="utf-8") as f:
    print(repr(f.read(10)))
    pos = f.tell()
    print(pos)

Output

2025-12-12 | O04344 | rating:3 | Late de
2 37
'2025-12-12'
10

How it works: readline() reads one line, readlines() reads all remaining lines into a list, and read(n) reads n characters. The file keeps an internal position, which tell() reports and seek() can change. After reading 10 characters the position is 10.

Reading the result: After one line and two more, 37 lines were left. The position after reading ten characters is 10.

Example 4: Writing a file

Question: Write a short report to a new file and read it back.

lines = ["FreshBasket report", "orders: 6000", "cities: 8"]

with open("report.txt", "w", encoding="utf-8") as f:
    f.write(lines[0] + "\n")
    f.write("\n".join(lines[1:]) + "\n")
    print("Revenue ₹35,33,834", file=f)

with open("report.txt", encoding="utf-8") as f:
    content = f.read()
print(content)

Output

FreshBasket report
orders: 6000
cities: 8
Revenue ₹35,33,834

How it works: Mode "w" creates the file, erasing any existing content. write adds exactly the text you give, so you must add \n yourself. Passing file=f to print is a convenient way to write a line with an automatic newline.

Reading the result: The saved report has 4 lines, including the one containing the ₹ symbol, which survived because we used UTF-8.

Example 5: Appending

Question: Add lines to the end of a log without losing the old ones.

for msg in ["job started", "loaded 6000 orders", "job finished"]:
    with open("job.log", "a", encoding="utf-8") as f:
        f.write(msg + "\n")

with open("job.log", encoding="utf-8") as f:
    log = f.read().splitlines()
print(log)

with open("job.log", "w", encoding="utf-8") as f:
    pass
print(open("job.log", encoding="utf-8").read() == "")

Output

['job started', 'loaded 6000 orders', 'job finished']
True

How it works: Mode "a" appends to the end and creates the file if needed. The final lines show the danger of mode "w": opening it and doing nothing empties the file. Common modes are "r" (read, the default), "w" (write), "a" (append), "x" (create, fail if it exists), and "b" added for binary files.

Reading the result: The log collected 3 separate messages: ['job started', 'loaded 6000 orders', 'job finished']. Then the "w" open wiped it clean, so the final check printed True.

Example 6: Paths with pathlib

Question: Build, test and inspect file paths in a portable way.

from pathlib import Path

p = Path("feedback.txt")
print(p.exists(), p.suffix, p.stem, p.name)
print(p.stat().st_size > 0)

out_dir = Path("output") / "reports"
out_dir.mkdir(parents=True, exist_ok=True)
report = out_dir / "summary.txt"
report.write_text("hello\n", encoding="utf-8")
print(report, report.read_text(encoding="utf-8").strip())

print(sorted(x.name for x in Path(".").glob("*.csv")))

Output

True .txt feedback feedback.txt
True
output/reports/summary.txt hello
['customers.csv', 'messy_orders.csv', 'order_items.csv', 'orders.csv', 'products.csv', 'stores.csv']

How it works: Path objects represent file locations and work on Windows, macOS and Linux alike. The / operator joins parts, .exists(), .suffix and .stem inspect a path, and read_text / write_text are shortcuts for small files. glob("*.csv") lists matching files.

Reading the result: The feedback file exists (True) with the suffix .txt, and the current folder contains 6 CSV files. Prefer Path over gluing strings with + and slashes.

Example 7: Parse the feedback file

Question: Convert each feedback line into a dictionary.

reviews = []
with open("feedback.txt", encoding="utf-8") as f:
    for line in f:
        date, order, rating, text = [part.strip() for part in line.split("|", 3)]
        reviews.append({
            "date": date,
            "order": order,
            "rating": int(rating.split(":")[1]),
            "text": text,
        })

print(len(reviews))
print(reviews[0])

Output

40
{'date': '2025-12-12', 'order': 'O04344', 'rating': 3, 'text': 'Late delivery by almost 50 minutes. Very disappointed. Call me on +91 92699 33648.'}

How it works: split("|", 3) cuts each line into at most four pieces (the review text could contain a |, so we limit the splits). A comprehension strips whitespace from every piece, rating:3 is split again on the colon, and the number is converted to int. The result is a list of dictionaries, the shape you learned in the dictionaries lesson.

Reading the result: We parsed 40 reviews. The first one is dated 2025-12-12 for order O04344 with rating 3.

Example 8: Summarise the reviews

Question: Find the average rating, the rating distribution and the most common complaint words.

from collections import Counter

ratings = [r["rating"] for r in reviews]
print(f"average: {sum(ratings) / len(ratings):.2f}")
print(sorted(Counter(ratings).items()))

words = Counter()
for r in reviews:
    if r["rating"] <= 2:
        for w in r["text"].lower().replace(".", "").split():
            if len(w) > 4:
                words[w] += 1
print(words.most_common(5))

Output

average: 3.05
[(1, 7), (2, 7), (3, 11), (4, 7), (5, 8)]
[('missing', 5), ('order', 5), ('refund', 5), ('requested,', 5), ('reference', 5)]

How it works: We reuse Counter for the distribution and for words in low-rated reviews, ignoring short words. This is a basic text-analytics pipeline built from file reading, splitting and counting.

Reading the result: The mean rating is 3.05 and the rating counts are [(1, 7), (2, 7), (3, 11), (4, 7), (5, 8)]. A proper tokeniser or regular expressions (a later lesson) would give cleaner words.

Example 9: Write the cleaned results

Question: Save only the low-rated reviews to a new file, with phone numbers hidden.

import re

low_reviews = [r for r in reviews if r["rating"] <= 2]

def hide_phone(text):
    return re.sub(r"\+?\d[\d ]{8,}\d", "[phone]", text)

with open("low_reviews.txt", "w", encoding="utf-8") as f:
    for r in sorted(low_reviews, key=lambda r: r["date"]):
        f.write(f'{r["date"]} {r["order"]} ★{r["rating"]} {hide_phone(r["text"])}\n')

with open("low_reviews.txt", encoding="utf-8") as f:
    saved = f.read().splitlines()
print(len(saved))
print(saved[0])

Output

14
2025-04-07 O05898 ★2 The Detergent 2kg was not fresh. Please contact me at nikhil.rao397@example.com or [phone].

How it works: We filter, sort by date, mask phone numbers with a small regular expression (explained properly in the regex lesson) and write each line. Reading the file back confirms what was saved.

Reading the result: 14 low-rated reviews were written, and the first reads 2025-04-07 O05898 ★2 The Detergent 2kg was not fresh. Please.... Privacy tip: always mask personal data before sharing text files.

Example 10: Files and errors

Question: What happens if the file does not exist?

from pathlib import Path

try:
    open("does_not_exist.txt", encoding="utf-8")
except FileNotFoundError as e:
    print("FileNotFoundError:", e.filename)

def read_lines(path):
    p = Path(path)
    if not p.exists():
        return []
    return p.read_text(encoding="utf-8").splitlines()

print(len(read_lines("feedback.txt")), len(read_lines("nope.txt")))

Output

FileNotFoundError: does_not_exist.txt
40 0

How it works: Opening a missing file for reading raises FileNotFoundError. You can either check first with exists() or catch the exception, which the next lesson covers in depth. A helper function keeps this decision in one place.

Reading the result: The missing file raised an error whose filename was does_not_exist.txt, while the helper returned 40 lines for the real file and 0 for the missing one.

Common mistakes

1. Forgetting encoding="utf-8", which can garble non-English characters on some systems.

2. Using mode "w" on a file you meant to append to, erasing it.

3. Forgetting newlines when writing: write("a") then write("b") gives ab.

4. Reading a huge file with read() instead of looping over lines.

5. Using the file after the with block.

with open("feedback.txt", encoding="utf-8") as f:
    pass
f.read()

Output

Traceback (most recent call last):
  File "example.py", line 3, in <module>
    f.read()
    ~~~~~~^^
ValueError: I/O operation on closed file.

Practice questions

Practice 1 (Easy): Count lines with a keyword

Count how many lines of feedback.txt contain the word "Late".

Show solution
with open("feedback.txt", encoding="utf-8") as f:
    n_late = sum("Late" in line for line in f)
print(n_late)

Output

2

sum over booleans counts the True values. 2 lines mention "Late".

Practice 2 (Medium): Ratings by month

Using the parsed reviews, compute the average rating for each month (the first seven characters of the date) and print them in order.

Show solution
by_month = {}
for r in reviews:
    by_month.setdefault(r["date"][:7], []).append(r["rating"])

for month in sorted(by_month):
    vals = by_month[month]
    print(month, len(vals), round(sum(vals) / len(vals), 2))

Output

2025-01 3 3.0
2025-03 1 5.0
2025-04 2 3.5
2025-06 2 2.5
2025-07 3 3.33
2025-08 2 3.0
2025-09 6 3.67
2025-10 1 4.0
2025-11 7 2.29
2025-12 5 3.2
2026-01 8 2.75

Grouping with setdefault and then averaging is the manual version of a pandas groupby. The reviews span 11 different months.

Practice 3 (Hard): A safe report writer

Write write_report(path, rows) that writes a list of (label, value) pairs, one per line as label: value, creating missing folders, and returns the number of lines written. Test it with a nested path.

Show solution
from pathlib import Path

def write_report(path, rows):
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    with p.open("w", encoding="utf-8") as f:
        for label, value in rows:
            f.write(f"{label}: {value}\n")
    return len(rows)

n = write_report("out/deep/kpis.txt", [("orders", 6000), ("cities", 8)])
print(n, Path("out/deep/kpis.txt").read_text(encoding="utf-8").splitlines())

Output

2 ['orders: 6000', 'cities: 8']

p.parent.mkdir(parents=True, exist_ok=True) creates every missing folder without complaining if they exist. The function wrote 2 lines.

Key takeaways

  • Use with open(path, encoding="utf-8") as f: so files always close.
  • Modes: "r" read, "w" write (erases), "a" append, "x" create only.
  • Loop over the file object to process one line at a time.
  • Remember \n when writing, and strip it when reading.
  • pathlib.Path makes paths portable and gives handy shortcuts.
  • Parse text lines into dictionaries, then summarise with Counter and comprehensions.

Frequently asked questions

Why use with instead of open() and close()?

with closes the file even when an error happens in the middle, which prevents lost data and locked files.

What does encoding="utf-8" do?

It tells Python how to translate bytes to characters. UTF-8 handles every language and symbol and is the standard for web and data files.

How do I read a file that is not text, such as an image?

Open it in binary mode, open(path, "rb"), and you will get bytes instead of strings. Libraries such as Pillow or pandas usually handle these formats for you.

What is the difference between Path.read_text() and open().read()?

They give the same result for text files. read_text is shorter and opens and closes the file in one call.

Where do relative paths start from?

From the program's current working directory, which Path.cwd() shows. Scripts launched from other folders may therefore fail to find files; build paths from __file__ or use absolute paths for robustness.

How do I read very large files?

Loop over the file object line by line, or use pandas.read_csv(..., chunksize=...) for tables, so the whole file never has to fit in memory.

Newsletter

Keep learning Python

One email a week with Python and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Ready to practise? Try the Python interview questions