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:
- Add two more players to the file. Check they are printed.
- Instead of printing the whole line,
split(",")it and printMax scored 30. - Find the best. Keep a
best_nameand abest_score(start it at 0), and every time round the loop, if this score is bigger, remember this one instead. After the loop, printTop score: Max with 30. You will needint(). - 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. - Now take out the
withthat 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.
- Make the level bigger: more rows, and longer ones. Every row must be the same length.
- Add a
^toTILESthat builds a red spike, and put some in the file. - Write a second level into
level2.txt. Then putlevel_number = 2at the top and open"level" + str(level_number) + ".txt", so changing the number changes the level. (Call the first onelevel1.txt.) - Put a player in with a
ptile, and move it with the@onKeyDownhandlers 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.