Skip to content

Writing files

Goal: save data from your program to a file, add to a file without losing what is there, and build a program that remembers its data between runs.

Why this matters

Reading files lets a program use data that already exists. Writing files lets it create data that lasts: a saved game, a diary, a to-do list that is still there tomorrow. Put the two together and your programs stop being throwaway scripts and start being tools you actually use. This lesson ends with exactly that: a to-do list app that saves itself.

Writing a file

open() takes a second argument called the mode. Leave it out, as you did last lesson, and the file is opened for reading. Pass "w" and it is opened for writing:

write_basic.py
with open("notes.txt", "w") as file:
    file.write("Remember to water the plants.\n")
    file.write("Call the dentist.\n")
    file.write("Finish Module 6.\n")

with open("notes.txt") as file:
    print(file.read())
Remember to water the plants.
Call the dentist.
Finish Module 6.

file.write() puts a string into the file. Two things differ from print(). First, it accepts only strings, so numbers must be converted with str() or put in an f-string. Second, it does not add a newline. You write the \n yourself at the end of each line, or everything lands on one line.

The second with block reads the file back, using what you learned last lesson, to prove it worked. Look in your folder and notes.txt is there. If it already existed, it has been replaced. Which brings us to the most important thing to know about "w".

Writing replaces everything

Opening a file with "w" empties it immediately, before you write anything:

write_overwrites.py
with open("notes.txt", "w") as file:
    file.write("This is the first version.\n")

with open("notes.txt", "w") as file:
    file.write("This is the second version.\n")

with open("notes.txt") as file:
    print(file.read())
This is the second version.

The first version is gone. This surprises everyone once, usually by opening a file they meant to read with "w" by mistake and watching its contents vanish. There is no undo. Be careful with "w", and keep backups of files you care about while you are learning.

Adding to the end

To keep what is there and add more, use mode "a", for append:

append.py
with open("log.txt", "a") as file:
    file.write("The program ran.\n")

with open("log.txt") as file:
    print(file.read())

Run it once:

The program ran.

Run it again:

The program ran.
The program ran.

Each run adds a line. If the file does not exist yet, "a" creates it, the same as "w". Append mode is how programs keep logs and diaries: they only ever add, so nothing is lost.

Writing a list

A list becomes a file with a loop and one write() per item:

write_list.py
items = ["eggs", "bread", "apples"]

with open("shopping_saved.txt", "w") as file:
    for item in items:
        file.write(item + "\n")

text = "\n".join(items)
print(text)
print("---")
print(", ".join(items))
eggs
bread
apples
---
eggs, bread, apples

The program also shows a shortcut. "\n".join(items) glues the items together with a newline between each, making one string with the whole list in it. .join() is a string method: the string it is called on is the separator, and the argument is the list to join. ", ".join(items) gives a tidy comma-separated line, which is handy for printing a list without the brackets and quotes.

The loop and .join() produce the same file, except that .join() puts nothing after the last item, so you would write "\n".join(items) + "\n" to end the file with a newline. Use whichever you find clearer.

Checking whether a file exists

A program that saves its data has a problem on the very first run: there is nothing to load yet, and reading a missing file crashes. Python's os module, which talks to the operating system, can check first:

exists.py
import os

print(os.path.exists("shopping.txt"))
print(os.path.exists("nothing.txt"))
True
False

This is the second module you have imported, after random. os.path.exists() returns True if the file is there. Module 7 shows a more general way to deal with things that might go wrong, but for missing files this check is all you need.

Project: a to-do list that remembers

Here is the pattern that every program with saved data follows: load at the start, work in memory, save at the end.

todo_app.py
import os

FILENAME = "todo.txt"

def load_tasks():
    """Return the saved tasks as a list, or an empty list if there is no file yet."""
    tasks = []
    if os.path.exists(FILENAME):
        with open(FILENAME) as file:
            for line in file:
                tasks.append(line.strip())
    return tasks

def save_tasks(tasks):
    """Write the tasks to the file, one per line."""
    with open(FILENAME, "w") as file:
        for task in tasks:
            file.write(task + "\n")

def show_tasks(tasks):
    if len(tasks) == 0:
        print("Nothing to do!")
    number = 1
    for task in tasks:
        print(f"{number}. {task}")
        number += 1

def show_menu():
    print()
    print("1. Show tasks")
    print("2. Add a task")
    print("3. Finish a task")
    print("4. Quit")
    return input("Choose: ")

tasks = load_tasks()
print(f"Loaded {len(tasks)} tasks.")

while True:
    choice = show_menu()
    if choice == "1":
        show_tasks(tasks)
    elif choice == "2":
        tasks.append(input("New task: "))
    elif choice == "3":
        show_tasks(tasks)
        number = int(input("Which number is done? "))
        finished = tasks.pop(number - 1)
        print(f"Finished: {finished}")
    elif choice == "4":
        save_tasks(tasks)
        print(f"Saved {len(tasks)} tasks. Goodbye.")
        break
    else:
        print("Please choose 1, 2, 3, or 4.")

A first run, with no file yet:

Loaded 0 tasks.

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 2
New task: buy milk

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 2
New task: call mum

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 4
Saved 2 tasks. Goodbye.

Run it again and it starts with Loaded 2 tasks. The list is in todo.txt, one task per line, and you can open the file in your editor to see it.

Walk through the pieces:

  • FILENAME = "todo.txt" sits at the top so the name appears once. A variable in capitals is a convention meaning this is a setting, do not change it while the program runs. Python does not enforce it, but every Python programmer reads it that way.
  • load_tasks() returns an empty list if the file is missing, otherwise the stripped lines. Either way the rest of the program gets a list and does not care which.
  • save_tasks() writes the whole list with "w". Replacing everything is right here, because the list in memory is the truth and the file should match it.
  • The menu loop only touches the tasks list. Files are handled in two small functions, and nothing else knows they exist. That is the same one job per function rule from Module 5.
  • Option 3 uses .pop(number - 1) because the user sees numbers starting at 1 but list indexes start at 0.

Saving only on quit is simple but fragile: close the window without quitting properly and the changes are lost. A safer version saves after every change. That is a one-line addition, and it is in the Try it below.

Try it

Make the to-do app save after every add and finish, not just on quit. Then add a fifth option that clears all tasks, asking Are you sure? first. Finally, notice what happens if the user types abc when asked which task is done, and think about how you would fix it with the tools you have. Module 7 will give you a better one.

Common mistakes

My file is empty, or my data disappeared

You opened it with "w", which empties the file before anything else happens. Use "a" to add, or leave the mode out to read.

Everything is on one line

write() does not add newlines. Write item + "\n".

TypeError: write() argument must be str, not int

write() only takes strings. Use file.write(str(number)) or an f-string.

The file exists but the program reads it as empty

The read happens before the write, or the file was written by a program that has not finished yet. Make sure the writing with block has ended before you open the file to read it.

PermissionError: [Errno 13] Permission denied

The file is open in another program, such as Excel, or you tried to write somewhere protected. Close the other program, or save to your own folder.

Exercises

  1. Save a shopping list. Ask for items until the user types done, then write them to shopping_saved.txt, one per line. Open the file in your editor to check.
  2. Diary. Ask for today's date and a sentence about the day. Append them to diary.txt as date: sentence, then print the whole diary. Run it several times.
  3. Numbered copy. Read story.txt and write story_numbered.txt, the same lines with 1., 2., and so on in front.
  4. High score. Take the guessing game and make it remember the best score in high_score.txt. On start, announce the record if there is one. After a game, save the score if it beats the record and say so.
Solution 1
items = []

while True:
    item = input("Add an item, or done to finish: ")
    if item == "done":
        break
    items.append(item)

with open("shopping_saved.txt", "w") as file:
    for item in items:
        file.write(item + "\n")

print(f"Saved {len(items)} items to shopping_saved.txt.")
Solution 2
date = input("Today's date: ")
entry = input("What happened today? ")

with open("diary.txt", "a") as file:
    file.write(f"{date}: {entry}\n")

print("Saved. Your diary so far:")
with open("diary.txt") as file:
    print(file.read())

Append mode does the remembering. The program never reads the diary before writing, because it does not need to.

Solution 3
number = 1

with open("story.txt") as source:
    with open("story_numbered.txt", "w") as target:
        for line in source:
            target.write(f"{number}. {line}")
            number += 1

print(f"Copied {number - 1} lines to story_numbered.txt.")

Two files are open at once, one with nested inside the other. The line is written without .strip(), so its own newline carries across and no \n needs adding.

Solution 4
import os
import random

FILENAME = "high_score.txt"

def load_high_score():
    """Return the saved best score, or 0 if there is none yet."""
    if not os.path.exists(FILENAME):
        return 0
    with open(FILENAME) as file:
        return int(file.read().strip())

def save_high_score(score):
    with open(FILENAME, "w") as file:
        file.write(f"{score}\n")

def ask_for_number(prompt):
    text = input(prompt)
    while not text.isdigit():
        print("Please type a whole number.")
        text = input(prompt)
    return int(text)

def play_game():
    secret = random.randint(1, 100)
    attempts = 0
    while True:
        guess = ask_for_number("Your guess: ")
        attempts += 1
        if guess < secret:
            print("Too low.")
        elif guess > secret:
            print("Too high.")
        else:
            print("Correct!")
            return attempts

best = load_high_score()
if best == 0:
    print("No high score yet. I am thinking of a number between 1 and 100.")
else:
    print(f"The record is {best} attempts. I am thinking of a number between 1 and 100.")

attempts = play_game()
print(f"You got it in {attempts} attempts.")

if best == 0 or attempts < best:
    save_high_score(attempts)
    print("New record!")

The load and save functions mirror the to-do app. 0 stands for no record yet, and the read value is stripped before int() because the file ends with a newline.

Summary

  • open(name, "w") writes and replaces the file. open(name, "a") appends. No mode means read.
  • file.write() takes one string and adds no newline. Write "\n" yourself.
  • "\n".join(items) turns a list into one string with newlines between items.
  • os.path.exists(name) tells you whether a file is there, after import os.
  • Programs with saved data load at the start, work in memory, and save at the end, or after every change.

Module 6 complete

Your programs can now remember. That is the difference between an exercise and a tool. Module 7 deals with what happens when things go wrong, so a bad line in a file or a wrong answer from the user no longer crashes everything.

Next: When things go wrong