Python Basics series — Part 8. This post continues Part 1: What Every Beginner Should Know, Part 2: Variables and Data Types, Part 3: Lists and Dictionaries, Part 4: If Statement, Part 5: User Input and While Loops, Part 6: Functions, and Part 7: Classes. Each post in the series takes one topic and covers it in depth. This one covers files and exceptions.

Every program in this series so far has had two weaknesses. First, it forgets everything when it ends: the projects you entered, the tasks you added, and the quotes you calculated disappear as soon as the program stops. Second, it’s fragile: one unexpected value — a word where a number should be, a missing key, a file that isn’t there — and the program crashes with an error.

This post fixes both. You’ll learn how to save data to files and load it back, including the CSV and JSON formats used by spreadsheets and APIs. And you’ll learn how to handle exceptions — Python’s errors — so your programs can recover from problems instead of crashing. You saw a first look at try and except in Part 5; here, you’ll learn the whole picture.

Run these examples as a script. Save each one in a .py file and run it with python3 your_file.py. Files are created in the folder you run the command from, so create a new practice folder and run your scripts from there.

Writing to a File

To work with a file, you open it with the built-in open() function. The cleanest way is with a with statement, which closes the file for you automatically when the block ends — even if an error happens inside it:

with open("notes.txt", "w", encoding="utf-8") as file:
    file.write("Call Bright Bakery about the menu page.\n")
    file.write("Send invoice to Valley Dental.\n")
    file.write("Back up client files.\n")

There’s a lot in that first line:

  • "notes.txt" is the file’s name. Because it’s not a full path, Python looks in the current folder.
  • "w" is the mode: write. It creates the file if it doesn’t exist — and erases everything in it if it does.
  • encoding="utf-8" tells Python how to store the text. UTF-8 handles every language and symbol (like ñ, é, or ✓) and is the standard on the web. Always include it, so your files work the same on every computer.
  • as file gives the open file a name you can use inside the block.

file.write() writes exactly the text you give it — it doesn’t add a line break like print() does. That’s why each line ends with \n, the newline character.

Adding to a File Instead of Replacing It

To add lines to the end of an existing file, open it in append mode, "a":

with open("notes.txt", "a", encoding="utf-8") as file:
    file.write("Renew the domain for Calexico Coffee.\n")

File Modes

ModeNameWhat it does
"r"ReadOpens an existing file for reading. This is the default. Error if the file doesn’t exist.
"w"WriteCreates the file, or erases it if it exists, then writes.
"a"AppendCreates the file if needed, then adds to the end.
"x"CreateCreates a new file. Error if the file already exists, so you can’t overwrite by accident.

Reading a File

To read the whole file at once, open it in read mode ("r", or leave the mode out) and call .read():

with open("notes.txt", encoding="utf-8") as file:
    contents = file.read()

print(contents)
Call Bright Bakery about the menu page.
Send invoice to Valley Dental.
Back up client files.
Renew the domain for Calexico Coffee.

Notice the blank line at the end: the file’s last line ends with \n, and print() adds one more.

Reading Line by Line

More often, you’ll want to work with one line at a time. Loop over the file directly:

with open("notes.txt", encoding="utf-8") as file:
    for number, line in enumerate(file, start=1):
        print(f"{number}. {line.strip()}")
1. Call Bright Bakery about the menu page.
2. Send invoice to Valley Dental.
3. Back up client files.
4. Renew the domain for Calexico Coffee.

Each line still includes its \n at the end, so call .strip() to remove it (otherwise you’d get an extra blank line between each one). This approach also works for huge files, because Python reads one line at a time instead of loading the whole file into memory.

To get all the lines as a list, use .read().splitlines(), which also removes the line breaks:

with open("notes.txt", encoding="utf-8") as file:
    tasks = file.read().splitlines()

print(len(tasks))   # 4
print(tasks[0])     # Call Bright Bakery about the menu page.

Numbers in Files Are Text

Everything you read from a text file is a string, just like with input() in Part 5. Convert numbers before you do math with them:

with open("visits.txt", "w", encoding="utf-8") as file:
    file.write("1200\n340\n210\n")

with open("visits.txt", encoding="utf-8") as file:
    visits = [int(line) for line in file]

print(sum(visits))   # 1750

int() ignores the spaces and newline around a number, so int("1200\n") works without calling .strip() first.

Working with Paths: pathlib

The pathlib module gives you a convenient way to work with files and folders. A Path object represents a location on your computer and comes with helpful methods:

from pathlib import Path

path = Path("notes.txt")

print(path.exists())   # True
print(path.suffix)     # .txt
print(path.stem)       # notes

text = path.read_text(encoding="utf-8")       # read the whole file in one line
Path("summary.txt").write_text("4 tasks\n", encoding="utf-8")   # write in one line

You can build paths with /, and Python uses the correct separator on Windows, macOS, and Linux:

from pathlib import Path

reports = Path("reports")
reports.mkdir(exist_ok=True)              # create the folder if it isn't there
report_file = reports / "january.txt"     # reports/january.txt
report_file.write_text("Revenue: $5,780\n", encoding="utf-8")

for txt_file in Path(".").glob("*.txt"):   # every .txt file in the current folder
    print(txt_file.name)

read_text() and write_text() are perfect for small files. For reading line by line or appending, use open() as shown above — Path objects work with open() too.

CSV Files: Spreadsheet Data

CSV (comma-separated values) is the simplest way to exchange data with spreadsheets: Excel and Google Sheets can open and export it. Each line is a row, and commas separate the columns. Python’s built-in csv module handles the details, like values that contain commas.

The most convenient tools are DictWriter and DictReader, which work with the lists of dictionaries from Part 3:

import csv

projects = [
    {"client": "Bright Bakery", "type": "Website", "budget": 2500},
    {"client": "Lopez Landscaping", "type": "SEO", "budget": 900},
    {"client": "Calexico Coffee", "type": "Website", "budget": 3200},
]

with open("projects.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.DictWriter(file, fieldnames=["client", "type", "budget"])
    writer.writeheader()
    writer.writerows(projects)

That creates a file you can open in any spreadsheet program:

client,type,budget
Bright Bakery,Website,2500
Lopez Landscaping,SEO,900
Calexico Coffee,Website,3200

Reading it back gives you one dictionary per row, using the header row as the keys:

import csv

with open("projects.csv", newline="", encoding="utf-8") as file:
    rows = list(csv.DictReader(file))

print(rows[0])   # {'client': 'Bright Bakery', 'type': 'Website', 'budget': '2500'}

total = sum(int(row["budget"]) for row in rows)
print(total)     # 6600

Two details to remember: always pass newline="" when opening CSV files (the csv module handles line endings itself), and every value comes back as a string — notice '2500' — so convert numbers before using them.

JSON Files: Saving Python Data

JSON is the format most APIs use, and it maps almost perfectly onto Python’s lists and dictionaries. That makes it the easiest way to save your program’s data and load it back later, with numbers and True/False values intact:

import json

settings = {
    "business": "Bright Bakery",
    "hourly_rate": 85,
    "rush_fee": 0.25,
    "services": ["Website", "SEO", "Hosting"],
    "accepting_clients": True,
}

with open("settings.json", "w", encoding="utf-8") as file:
    json.dump(settings, file, indent=2)

with open("settings.json", encoding="utf-8") as file:
    loaded = json.load(file)

print(loaded["hourly_rate"] * 2)   # 170   (still a number!)
print(loaded["services"][0])       # Website
  • json.dump(data, file) writes Python data to a file. indent=2 formats it nicely so people can read it too.
  • json.load(file) reads it back into Python lists and dictionaries.

(Don’t confuse these with json.dumps() and json.loads() from Part 3 — the extra s stands for string. Those convert to and from a string instead of a file.)

What Is an Exception?

When something goes wrong while a program runs, Python raises an exception: an error object that describes what happened. If nothing handles it, the program stops and prints a traceback:

prices = {"Basic": 499, "Business": 1499}
print(prices["Premium"])
Traceback (most recent call last):
  File "/Users/you/practice/prices.py", line 2, in <module>
    print(prices["Premium"])
          ~~~~~~^^^^^^^^^^^
KeyError: 'Premium'

Read a traceback from the bottom up. The last line tells you the type of exception (KeyError) and a message ('Premium'). The lines above it show where it happened: the file, the line number, and the code on that line.

You’ve already met several exception types in this series. These are the ones beginners run into most:

ExceptionWhen it happensExample
ValueErrorA value has the right type but the wrong contentint("five")
TypeErrorAn operation gets the wrong type"Pages: " + 5
KeyErrorA dictionary key doesn’t existclient["phone"]
IndexErrorA list index is out of rangepages[10]
ZeroDivisionErrorDividing by zerototal / 0
FileNotFoundErrorOpening a file that doesn’t existopen("missing.txt")
NameErrorUsing a variable that was never definedprint(totl)

Handling Exceptions with try and except

To handle an exception, put the code that might fail in a try block, and the code to run if it does fail in an except block:

try:
    with open("clients.txt", encoding="utf-8") as file:
        print(file.read())
except FileNotFoundError:
    print("No clients yet. Add your first client to get started.")

If clients.txt exists, its contents are printed and the except block is skipped. If it doesn’t, Python jumps to the except block instead of crashing, and the program keeps running.

Catch Specific Exceptions

Always name the exception you expect. Different problems deserve different responses, and you can have several except blocks:

def average_budget(total, count_text):
    try:
        count = int(count_text)
        return total / count
    except ValueError:
        print(f"'{count_text}' isn't a whole number.")
    except ZeroDivisionError:
        print("Can't average zero projects.")
    return None

print(average_budget(6600, "3"))     # 2200.0
print(average_budget(6600, "three"))
print(average_budget(6600, "0"))
2200.0
'three' isn't a whole number.
None
Can't average zero projects.
None

Getting the Error Message with as

Add as and a name to get the exception object itself. Printing it shows Python’s message:

try:
    pages = int("five")
except ValueError as error:
    print(f"Couldn't convert: {error}")
# Couldn't convert: invalid literal for int() with base 10: 'five'

else and finally

A try statement can have two more optional parts:

  • else runs only if the try block didn’t raise an exception. It’s the place for code that should run only when everything worked.
  • finally runs no matter what — success, handled error, or even an unhandled one. It’s used for cleanup that must always happen.
def load_rate(path):
    try:
        with open(path, encoding="utf-8") as file:
            rate = float(file.read())
    except FileNotFoundError:
        print("Rate file not found. Using the default.")
        rate = 85.0
    except ValueError:
        print("Rate file is damaged. Using the default.")
        rate = 85.0
    else:
        print("Rate loaded from file.")
    finally:
        print("Done checking the rate.")
    return rate

with open("rate.txt", "w", encoding="utf-8") as file:
    file.write("95")

print(load_rate("rate.txt"))
print(load_rate("no_such_file.txt"))
Rate loaded from file.
Done checking the rate.
95.0
Rate file not found. Using the default.
Done checking the rate.
85.0

With with statements handling file cleanup for you, you won’t need finally often — but it’s good to recognize.

Don’t Catch Everything

It’s tempting to write a bare except: (or except Exception:) that catches every possible error. Avoid it: it also hides bugs you’d want to know about, like a misspelled variable name.

try:
    total = sum(budgets)       # oops: the variable is called "budget_list"
except:
    print("Couldn't calculate the total.")

This prints a vague message instead of the real problem — a NameError pointing straight at the typo. Catch only the exceptions you expect and know how to handle, and let everything else show its traceback while you’re developing.

Failing Silently

Sometimes the right response to an error is to do nothing and move on. Use pass, which is Python’s “do nothing” statement:

lines = ["1200", "340", "n/a", "210", ""]
visits = []

for line in lines:
    try:
        visits.append(int(line))
    except ValueError:
        pass   # skip anything that isn't a number

print(visits)   # [1200, 340, 210]

Use this deliberately, when skipping bad data really is what you want — and consider counting or logging what you skipped, so problems don’t go unnoticed.

Raising Your Own Exceptions

Exceptions aren’t just for Python’s built-in errors. Your own functions and classes from Part 6 and Part 7 can raise them when they receive something they can’t work with. Use the raise keyword with an exception type and a clear message:

def calculate_quote(pages, hourly_rate=85):
    if pages < 1:
        raise ValueError("A project needs at least 1 page.")
    return pages * 4 * hourly_rate

try:
    print(calculate_quote(5))   # 1700
    print(calculate_quote(0))
except ValueError as error:
    print(f"Error: {error}")    # Error: A project needs at least 1 page.

Raising an exception is better than returning a strange value like -1 or None, because the problem can’t be quietly ignored. The code that calls your function decides how to handle it: show a message, ask the user again, or let the program stop.

Putting It All Together: A Client List That Remembers

Here’s a small program that keeps a client list in a JSON file. Each time you run it, it loads the clients you saved before, lets you add more, and saves them again. It handles a missing file, a damaged file, and invalid input without crashing:

import json
from pathlib import Path

DATA_FILE = Path("clients.json")


def load_clients():
    """Return the saved clients, or an empty list if there aren't any."""
    try:
        with open(DATA_FILE, encoding="utf-8") as file:
            return json.load(file)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        print(f"Warning: {DATA_FILE} is damaged. Starting with an empty list.")
        return []


def save_clients(clients):
    """Save the clients to the data file."""
    with open(DATA_FILE, "w", encoding="utf-8") as file:
        json.dump(clients, file, indent=2)


def ask_budget():
    """Ask for a budget until the user enters a valid, positive number."""
    while True:
        answer = input("Budget: $").strip().replace(",", "")
        try:
            budget = float(answer)
        except ValueError:
            print("Please enter a number, like 2500.")
            continue
        if budget <= 0:
            print("The budget must be more than zero.")
            continue
        return budget


clients = load_clients()
print(f"Loaded {len(clients)} saved client(s).")

while True:
    name = input("Client name (or press Enter to finish): ").strip()
    if not name:
        break
    clients.append({"name": name, "budget": ask_budget()})

save_clients(clients)

print("\nAll clients:")
for client in clients:
    print(f"  {client['name']}: ${client['budget']:,.2f}")

The first run starts with no file:

Loaded 0 saved client(s).
Client name (or press Enter to finish): Bright Bakery
Budget: $2,500
Client name (or press Enter to finish): Valley Dental
Budget: $three hundred
Please enter a number, like 2500.
Budget: $-300
The budget must be more than zero.
Budget: $300
Client name (or press Enter to finish): 

All clients:
  Bright Bakery: $2,500.00
  Valley Dental: $300.00

Run it again, and the clients from last time are still there:

Loaded 2 saved client(s).
Client name (or press Enter to finish): Calexico Coffee
Budget: $3200
Client name (or press Enter to finish): 

All clients:
  Bright Bakery: $2,500.00
  Valley Dental: $300.00
  Calexico Coffee: $3,200.00

Open clients.json in your editor and you’ll see your data in plain, readable JSON. Try deleting a bracket from it to damage it, and run the program again to see the warning.

Common Beginner Mistakes

Opening a file in "w" mode when you meant "a". Write mode erases the file the moment it’s opened, even if you never write anything. Use "a" to add to a file, and keep backups of important data while you’re learning.

Forgetting to close a file. If you use file = open(...) without with, you have to call file.close() yourself, or your data might not be fully saved. Using with handles it automatically.

Getting the path wrong. A file name like "notes.txt" is looked up in the folder you ran the program from, not necessarily the folder the script is in. If you get a FileNotFoundError for a file you can see, check which folder your terminal is in, or use the full path.

Forgetting the newline. write() doesn’t add \n for you, so everything ends up on one line.

Forgetting that file data is text. Numbers read from text and CSV files are strings. Convert them with int() or float() before doing math. (JSON keeps numbers as numbers.)

Leaving out the encoding. Without encoding="utf-8", a file containing accented letters or symbols may be saved or read differently on another computer.

Catching every exception. A bare except: hides real bugs. Catch the specific exceptions you expect.

Putting too much in the try block. Keep the try block to the line or two that might fail. If the try covers 30 lines, you can’t tell which one raised the error, and you might handle an exception you didn’t mean to.

Practice Exercises

  1. Write a program that asks the user for their favorite quote and appends it to quotes.txt. Run it a few times, then write a second program that prints every quote with a number in front of it.
  2. Write a function count_lines(path) that returns how many lines a text file has, or 0 if the file doesn’t exist.
  3. Write a program that reads a file of numbers, one per line, skips any lines that aren’t numbers, and prints the total, the average, and how many lines were skipped.
  4. Create a CSV file of products with name and price columns. Read it with csv.DictReader and print the most expensive product.
  5. Save a dictionary of your settings to settings.json, then write a second program that loads the settings and uses a default value for any setting that’s missing.
  6. Add a ValueError to the Project class from Part 7 so that creating a project with fewer than 1 page raises an error with a helpful message.

Conclusion

Files let your programs remember. With open() and a with statement, you can write and read text files; with the csv and json modules, you can exchange data with spreadsheets and save your program’s lists and dictionaries exactly as they are. pathlib makes working with file paths simple and reliable on every operating system.

Exceptions let your programs survive the unexpected. Read tracebacks from the bottom up, wrap the code that might fail in try, handle the specific exceptions you expect in except, and use else and finally when you need them. When your own code receives something it can’t handle, raise a clear exception instead of guessing.

Together with everything else in this series, you now have the tools to build complete, practical programs: ones that take input, make decisions, organize their logic into functions and classes, keep their data between runs, and handle problems gracefully.

Next in the Python Basics series: Testing Your Code, the final part: how to prove your code works with automated tests.

Python Basics series: Part 1: What Every Beginner Should Know · Part 2: Variables and Data Types · Part 3: Lists and Dictionaries · Part 4: If Statement · Part 5: User Input and While Loops · Part 6: Functions · Part 7: Classes · Part 8: Files and Exceptions · Part 9: Testing Your Code