Lesson 9 — Files

By the end of this you can: write text into a file and read it back, all at once, a line at a time, or one line when you ask for it; cut each line into its parts; add to the end of a file without losing what is there; and build a game's level out of one.

A variable forgets

Everything so far has lived in variables, and a variable lasts exactly as long as the program does. Press Stop and every score, every name, every list is gone. Run it again and it all starts from the beginning.

A file is text kept somewhere else, under a name, and it stays there when the program ends. The next program — or the same one, run again — can open it and read what the last one wrote. High score tables, saved games, level maps and server logs are all files.

On your own computer a file is one you can see: it appears in the folder your program runs in, and you can open it in any text editor. In the playground it is just as real, but it lives inside the page. It lasts from one Run to the next, and it is gone when you reload.

Writing a file

with open("names.txt", "w") as names_file:
    names_file.write("Ada\n")
    names_file.write("Max\n")
    names_file.write("Zoe\n")

with open("names.txt", "r") as names_file:
    print(names_file.read())
Ada
Max
Zoe

Read the first line out loud: with the file names.txt, open to write, as names_file. The indented lines underneath can use it by that name. When the indenting ends, Python closes the file for you — you have finished with it, and everything you wrote is safely in it.

The second thing in open() says what you are going to do:

  • "w" — write. Makes the file, or empties it if it is already there.
  • "r" — read. The file has to be there already.
  • "a" — add to the end. You'll meet it further down.

write() puts text in. read() hands back everything in the file as one string.

Look at what went in: "Ada\n", not "Ada". \n is how you write the end of a line in a string — it's one character, a newline. print() adds one for you; write() does not, and puts in exactly what you give it and nothing more:

with open("names.txt", "w") as names_file:
    names_file.write("Ada")
    names_file.write("Max")
    names_file.write("Zoe")

with open("names.txt", "r") as names_file:
    print(names_file.read())
AdaMaxZoe

No error. Three names, one line, no gaps.

"w" starts again

Opening a file with "w" empties it first, if it was already there:

with open("notes.txt", "w") as notes_file:
    notes_file.write("the first note\n")

with open("notes.txt", "w") as notes_file:
    notes_file.write("the second note\n")

with open("notes.txt", "r") as notes_file:
    print(notes_file.read())
the second note

The first note is gone. That is what you want for a file that holds one thing — the latest save — and exactly what you don't want for a list you are adding to. For that there is "a", further down.

Everything inside the with

Once the indenting ends, the file is closed. The name is still there, but the file behind it is shut:

with open("names.txt", "w") as names_file:
    names_file.write("Ada\n")

names_file.write("Max\n")
ValueError: I/O operation on closed file.

"I/O" is input and output — reading and writing. Anything you do with a file goes in the indented lines under its with. If you need it again later, open it again with another with.

Only text goes in

A file holds text and nothing else. Give write() a number:

score = 42

with open("score.txt", "w") as score_file:
    score_file.write(score)
TypeError: write() argument must be str, not int

str is Python's name for text, so this says it wanted text and got a whole number. str(score) turns the number into text first.

Coming back out, it is still text, even if it looks like a number:

score = 42

with open("score.txt", "w") as score_file:
    score_file.write(str(score) + "\n")

with open("score.txt", "r") as score_file:
    saved = score_file.read()

print(saved + 8)
TypeError: can only concatenate str (not "int") to str

saved is "42\n", and you can't add 8 to some text. int() turns it back into a number — and it doesn't mind the \n on the end:

with open("score.txt", "w") as score_file:
    score_file.write("42\n")

with open("score.txt", "r") as score_file:
    saved = int(score_file.read())

print(saved + 8)
50

So: str() on the way in, int() on the way out.

Notice that saved was made inside the with and used after it. That's fine: the file closes when the indenting ends, but a variable you filled from it keeps its value.

Reading line by line

A for loop over a file goes round once for every line in it, top to bottom:

with open("names.txt", "w") as names_file:
    names_file.write("Ada\n")
    names_file.write("Max\n")
    names_file.write("Zoe\n")

with open("names.txt", "r") as names_file:
    for line in names_file:
        print(line)
Ada

Max

Zoe

There's a gap after every name. Each line still has its \n on the end, and print() adds another, so every line ends twice.

strip() takes the \n off, and any spaces at either end. Here the names go into a list:

with open("names.txt", "w") as names_file:
    names_file.write("Ada\n")
    names_file.write("Max\n")
    names_file.write("Zoe\n")

names = []
with open("names.txt", "r") as names_file:
    for line in names_file:
        names.append(line.strip())

print(names)
print("Max" in names)
['Ada', 'Max', 'Zoe']
True

Forget the strip(), and the list looks almost the same:

with open("names.txt", "w") as names_file:
    names_file.write("Ada\n")
    names_file.write("Max\n")
    names_file.write("Zoe\n")

names = []
with open("names.txt", "r") as names_file:
    for line in names_file:
        names.append(line)

print(names)
print("Max" in names)
['Ada\n', 'Max\n', 'Zoe\n']
False

"Max" is not the same string as "Max\n", so "Max" in names is False, even though Max is plainly in the file. On the screen of a game you would never spot it, because a \n can't be seen. This is the most common mistake there is with files, and there is never an error message to tell you.

One line when you ask

readline() reads one line, and only one. The file remembers how far it has got, so the next readline() hands back the line after it. repr() shows a string the way you would type it, quotes and \n included, so you can see what is really in it:

with open("log.txt", "w") as log_file:
    log_file.write("first\n")
    log_file.write("second\n")

with open("log.txt", "r") as log_file:
    print(repr(log_file.readline()))
    print(repr(log_file.readline()))
    print(repr(log_file.readline()))
'first\n'
'second\n'
''

At the end there is nothing left to hand back, and readline() gives '', an empty string. A blank line in the middle of a file is '\n', never '', so '' always means the end.

A while loop that reads until then:

with open("log.txt", "w") as log_file:
    log_file.write("01:07,zoe,ok\n")
    log_file.write("01:12,ada,failed\n")
    log_file.write("01:20,max,ok\n")

with open("log.txt", "r") as log_file:
    line = log_file.readline()
    while line != "":
        print("read:", line.strip())
        line = log_file.readline()

print("the end of the file")
read: 01:07,zoe,ok
read: 01:12,ada,failed
read: 01:20,max,ok
the end of the file

The for loop is shorter, so why ever do this? Because with readline() you decide when the next line comes: one line now, something else, and the next line later — the next line of dialogue when the player presses a key, say.

Cutting a line up

A line usually holds more than one thing. Put a comma between them, and split(",") cuts the line at every comma and hands back a list:

line = "01:12,ada,failed\n"

parts = line.strip().split(",")

print(parts)
print(parts[1], "logged in at", parts[0])
['01:12', 'ada', 'failed']
ada logged in at 01:12

Strip first, then split — otherwise the last part is "failed\n". With a number in it, int() the part that is a number:

with open("scores.txt", "w") as scores_file:
    scores_file.write("Ada,12\n")
    scores_file.write("Max,30\n")
    scores_file.write("Zoe,21\n")

total = 0
with open("scores.txt", "r") as scores_file:
    for line in scores_file:
        parts = line.strip().split(",")
        name = parts[0]
        score = int(parts[1])
        print(name, "scored", score)
        total = total + score

print("total:", total)
Ada scored 12
Max scored 30
Zoe scored 21
total: 63

Adding to the end

"a" is for add (Python calls it append). It opens the file and adds to the end of what is there, instead of emptying it first:

with open("scores.txt", "w") as scores_file:
    scores_file.write("Ada,12\n")

with open("scores.txt", "a") as scores_file:
    scores_file.write("Max,30\n")

with open("scores.txt", "a") as scores_file:
    scores_file.write("Zoe,21\n")

with open("scores.txt", "r") as scores_file:
    print(scores_file.read())
Ada,12
Max,30
Zoe,21

If the file isn't there yet, "a" makes it, just like "w".

When the file is not there

"w" and "a" make a file that isn't there. "r" can't:

with open("save.txt", "r") as save_file:
    print(save_file.read())
FileNotFoundError: [Errno 44] No such file or directory: 'save.txt'

That is the message in the playground. On your own computer the number is 2 instead of 44, and the rest is the same. Check the name for a typo first: "Save.txt" and "save.txt" are two different files.

The long way: open() and close()

You will see files opened without with in other people's code, and in a lot of books:

names_file = open("names.txt", "w")
names_file.write("Ada\n")
names_file.write("Max\n")
names_file.close()

names_file = open("names.txt", "r")
print(names_file.read())
names_file.close()
Ada
Max

It is the same open, use, close — with the close written out by hand. It works, as long as you never forget the close(). Forget it, and what you wrote may not be in the file yet: Python saves writing up and does it all at once, when the file is closed. Here the file is read before that has happened:

names_file = open("names.txt", "w")
names_file.write("Ada\n")
names_file.write("Max\n")

reader = open("names.txt", "r")
print(repr(reader.read()))
reader.close()

names_file.close()
''

Empty, though two names were written. That can't happen with with, because the file is always closed when the indenting ends. That is why this lesson, and every game on this site that uses files, opens them with with.

A level from a file

The level in lesson 8 was a list of strings, one for each row. A file is lines of text — the same thing, kept outside the program:

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])

# The level, written into a file. A real game would ship it beside the
# program; the playground has no files of its own, so this one makes it.
with open("level.txt", "w") as level_file:
    level_file.write("########\n")
    level_file.write("#o....o#\n")
    level_file.write("#..oo..#\n")
    level_file.write("########\n")

# Read it back into a list of strings, one for each row.
layout = []
with open("level.txt", "r") as level_file:
    for line in level_file:
        layout.append(line.strip())

TILES = {
    "#": lambda: [rect(48, 48), color(90, 90, 110), area(), "wall"],
    "o": lambda: [circle(12), color(240, 200, 60), area(), "coin"],
}

addLevel(layout, {"tileWidth": 48, "tileHeight": 48, "tiles": TILES})

print(layout)
print("walls:", len(get("wall")))
print("coins:", len(get("coin")))
['########', '#o....o#', '#..oo..#', '########']
walls: 20
coins: 4

Change a line in the file and the level changes, without touching the code that builds it. That's how a game with fifty levels keeps them: fifty little files, and one program that reads whichever one comes next.

For a score that has to survive the page being reloaded, kaypy has setData() and getData(), which keep it in the browser even after the page is closed. A file in the playground lasts until you reload.

Watch for

Everything you do with a file goes inside its with. When the indenting ends, the file is closed.

write() doesn't end the line. Put "\n" on the end yourself.

Every line you read still has its \n. strip() it, every time, before you compare it with anything.

"w" empties the file. "a" adds to the end. "r" needs it to be there already.

Only text goes in and comes out. str() on the way in, int() on the way out.

readline() gives "" at the end, and only at the end.

Most of those are in Log Hacker, a game built step by step around three files it writes and then reads — and its "break it" boxes are a forgotten \n, a forgotten strip(), "w" where "a" should be, and writing to a file after its with has closed it.

Classwork 1 — The High Score Table

Start here and press Run:

with open("scores.txt", "w") as scores_file:
    scores_file.write("Ada,12\n")
    scores_file.write("Max,30\n")
    scores_file.write("Zoe,21\n")

with open("scores.txt", "r") as scores_file:
    for line in scores_file:
        print(line.strip())
Ada,12
Max,30
Zoe,21

Then, running after each step:

  1. Add two more players to the file. Check they are printed.
  2. Instead of printing the whole line, split(",") it and print Max scored 30.
  3. Find the best. Keep a best_name and a best_score (start it at 0), and every time round the loop, if this score is bigger, remember this one instead. After the loop, print Top score: Max with 30. You will need int().
  4. Ask for a new name and score with input() from lesson 2, and add them to the end of the file with "a", before it is read.
  5. Now take out the with that writes the file with "w". The file is still there from the last Run, so every Run adds one more player to it. Run it three times.

Stretch: write the top score into a second file, best.txt, and print New record! when a new score beats the one saved in it. The first time, best.txt isn't there — make it once with "w" before you take that part out.

Classwork 2 — Your Level in a File

Start from the level above.

  1. Make the level bigger: more rows, and longer ones. Every row must be the same length.
  2. Add a ^ to TILES that builds a red spike, and put some in the file.
  3. Write a second level into level2.txt. Then put level_number = 2 at the top and open "level" + str(level_number) + ".txt", so changing the number changes the level. (Call the first one level1.txt.)
  4. Put a player in with a p tile, and move it with the @onKeyDown handlers from lesson 7.

Stretch: make a scene that takes the level number, and when every coin is collected, go() to the next one. A new level is now a new file, and no new code.

Check yourself

What does this print?

with open("count.txt", "w") as count_file:
    count_file.write("1\n2\n3\n")

with open("count.txt", "r") as count_file:
    for line in count_file:
        print(line.strip())
    for line in count_file:
        print("again", line.strip())
1
2
3
Why doesn't the second loop print anything?

An open file remembers how far it has been read, just like it does for readline(). The first for loop read every line, so the file is at the end. The second one asks for the next line, and there isn't one — the same "" that readline() gives at the end — so it goes round no times at all. No error, nothing printed.

To read it again, open it again, with a second with. A file always opens at the top. That is why Log Hacker opens its log again every time it shows it: after each guess it reads the whole of log.txt from the top.


← Lesson 8 — Dictionaries · All nine lessons