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:
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())
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:
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())
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:
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:
Run it again:
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:
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))
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:
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.
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
taskslist. 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¶
- Save a shopping list. Ask for items until the user types
done, then write them toshopping_saved.txt, one per line. Open the file in your editor to check. - Diary. Ask for today's date and a sentence about the day. Append them to
diary.txtasdate: sentence, then print the whole diary. Run it several times. - Numbered copy. Read
story.txtand writestory_numbered.txt, the same lines with1.,2., and so on in front. - 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
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, afterimport 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.