Chapter 1
Opening and Closing Files
Working with a file in Python always has the same three beats. You open it, you do your thing, then you close it. This chapter is the open and the close: opening a file so it does what you meant, and closing it so your data actually sticks.
We won't pull the reading log apart into rows yet. For now the file is just a lump of text, and the job is getting that text into your program and back out again.
Opening a file
Here's the smallest useful thing you can do with the log. Open it, read it, and print the start of what you got.
f = open("reading-log.csv")
contents = f.read()
f.close()
print(contents[:28])
title,author,rating,finishedThree lines, three steps.
open("reading-log.csv") opens the file and hands back a file object. The
file object isn't the contents of the file. It's a handle: a small object that
knows which file you mean and where you're up to in it. You do your reading and
writing through the handle.
f.read() asks the handle for the whole file as one string. The reading log is
small, so that's fine. For a file that might be large, reading it all at once is
a mistake, and the next chapter covers what to do instead.
f.close() tells the operating system you're done. We'll come back to why that
line isn't optional.
The "reading-log.csv" here is a bare filename with no folder in front of it.
Python looks for it in whatever directory your script is running from. That's
enough to get started; Chapter 5 deals with paths properly.
Reading part of a file
f.read() with no argument reads everything from the current position to the
end. Give it a number and it reads that many characters and stops:
with open("reading-log.csv") as f:
start = f.read(12)
rest = f.read(16)
print(repr(start))
print(repr(rest))
'title,author'
',rating,finished'The handle remembers where it left off. The first read returns the first
twelve characters, and the second read picks up from character thirteen. That's
the position the file object tracks for you.
What the handle knows
A file object can tell you a few things about itself:
with open("reading-log.csv") as f:
print(f.name)
print(f.mode)
reading-log.csv
rf.mode is r because we didn't ask for anything else. That's the next topic.
Modes: what you plan to do with the file
open takes an optional second argument, the mode, which says what you
intend to do. Leave it out, as above, and you get "r", for reading.
| Mode | Opens the file for | If it already exists | If it does not exist |
|---|---|---|---|
"r" |
reading (the default) | reads from the start | raises FileNotFoundError |
"w" |
writing | empties it first | creates it |
"a" |
writing | keeps it, adds to the end | creates it |
"x" |
writing | raises FileExistsError |
creates it |
The three writing modes differ only in how they treat a file that's already
there. "w" throws the old contents away. "a" leaves them and appends. "x"
refuses, which is handy when overwriting something by accident would be bad.
Write a line to a new file:
with open("today.txt", "w") as f:
f.write("Started The Three-Body Problem\n")
with open("today.txt") as f:
print(f.read(), end="")
Started The Three-Body Problemf.write doesn't add a newline for you, so the \n at the end of that string is
doing real work. Without it, the next thing you write would run straight on from
Problem.
That example used with, which we haven't explained yet. Ignore it for one more
section; the point here is the mode.
Open the same file in "a" mode and the new line lands after the old one:
with open("today.txt", "a") as f:
f.write("Finished Piranesi\n")
with open("today.txt") as f:
print(f.read(), end="")
Started The Three-Body Problem
Finished Piranesi"x" mode is the careful one. today.txt exists now, so opening it with "x"
stops you:
with open("today.txt", "x") as f:
f.write("this line is never written")
That raises FileExistsError. Nothing is written, and the existing file is left
alone.
When the file isn't there
Open a file that doesn't exist in a reading mode and Python won't invent one for you:
open("wishlist.csv")
That's a FileNotFoundError. It catches people who expect open to create the
file. It doesn't. Only the writing modes ("w", "a", "x") create a file
that's not there.
Often a missing file isn't something you want to crash on. Maybe the wishlist just hasn't been started yet. Catch the exception and carry on with a sensible default:
try:
with open("wishlist.csv") as f:
wishlist = f.read()
except FileNotFoundError:
wishlist = ""
print(f"wishlist is {len(wishlist)} characters")
wishlist is 0 charactersCatch FileNotFoundError specifically, not every exception. If the file is there
but unreadable for some other reason, you want to hear about that.
Closing a file, and why it matters
Back to f.close(). It's easy to leave out, and on a small script you often get
away with it. Here's what you're risking when you do.
When you write to a file, the data doesn't always go to disk straight away. Python and the operating system hold some of it in memory and flush it out in batches, because lots of tiny writes are slow. Closing the file flushes whatever is left. Skip the close and your last few writes can simply not be there.
An open file also ties up resources. The operating system limits how many files one program can have open at once. On Windows, an open file can stop another program, or your own code, from touching it. Open files in a loop without closing them and a long-running program will eventually fail.
You can ask a file object whether it's closed:
f = open("reading-log.csv")
print("closed?", f.closed)
f.close()
print("closed?", f.closed)
closed? False
closed? TrueThe real problem with a manual close is that it doesn't run if something goes
wrong first:
f = open("reading-log.csv")
rows = parse(f.read()) # if this line raises an error
f.close() # this line is skipped
If parse fails, the exception jumps past f.close() and the file stays open.
You could wrap it in try and finally, but there's a cleaner way built for
exactly this.
with: the way you actually do it
A with block opens the file, gives it to you as f for the length of the
block, and closes it on the way out. It closes it whether the block finishes
normally or raises an error.
with open("reading-log.csv") as f:
contents = f.read()
print("closed?", f.closed)
print(contents[:28])
closed? True
title,author,rating,finishedNotice that f still exists after the block. It hasn't disappeared; it's just
closed, so you can't read from it any more. What you keep is contents, the
string you pulled out while the file was open.
You can open more than one file in a single with by separating them with a
comma. That's how you copy one file's contents into another:
with open("reading-log.csv") as source, open("backup.csv", "w") as target:
target.write(source.read())
with open("backup.csv") as f:
print(f.readline(), end="")
title,author,rating,finishedBoth files close when the block ends, in reverse order.
This is the form to use every time. From here on, every example in the book opens
files with with, and so should you. The only reason to call open and close
by hand is to understand what with is doing for you, which you now do.
Common mistakes
Opening in "w" when you meant "a". "w" empties the file the moment you
open it, before f.write runs at all:
with open("today.txt", "w") as f:
pass
with open("today.txt") as f:
print(repr(f.read()))
''today.txt had two lines a moment ago. Opening it in "w" threw them away. If
you meant to add to a file, the mode is "a".
Reading from a file after the with block. Once the block ends the file is
closed, and a closed file object won't read:
with open("reading-log.csv") as f:
pass
f.read()
That raises ValueError with the message I/O operation on closed file. Do your
reading inside the block, and keep the result in a variable, the way contents
kept the text in the earlier examples.
Forgetting the mode when you meant to write. open("notes.txt") with no mode
is read-only. Call .write on it and you get io.UnsupportedOperation: not
writable. The fix is to pass "w" or "a".
Practice
Try each of these before you read the solution under it.
- Copy
reading-log.csvtolog-copy.csvusing the two-filewithform, then open the copy in"a"mode and add a row for a book you finished recently. Read the copy back and print how many lines it has. - Write
line_count(path)that returns the number of lines in a file, and returns0rather than raising if the file does not exist. - Write
create_if_new(path)that creates the file empty if it is not there, and prints a message instead of crashing if it already exists.
Solutions
1.
with open("reading-log.csv") as src, open("log-copy.csv", "w") as dst:
dst.write(src.read())
with open("log-copy.csv", "a") as f:
f.write("Dune,Frank Herbert,5,2026-09-05\n")
with open("log-copy.csv") as f:
print(len(f.readlines()), "lines")
12 lines2. Catch FileNotFoundError and return the default. The _ is a throwaway
name for the loop variable, since you only want the count.
def line_count(path):
try:
with open(path) as f:
n = 0
for _ in f:
n += 1
return n
except FileNotFoundError:
return 0
print(line_count("reading-log.csv"))
print(line_count("nope.csv"))
11
03. "x" mode does the "only if new" check for you. Catch FileExistsError
for the other case.
def create_if_new(path):
try:
with open(path, "x"):
pass
print(f"created {path}")
except FileExistsError:
print(f"{path} already exists")
create_if_new("fresh.txt")
create_if_new("fresh.txt")
created fresh.txt
fresh.txt already existsWhere this leaves you
You can open a file for reading, writing, or appending; you know which modes
create a file and which expect it to exist already; you can read a whole file or
part of one; and you know why the file has to be closed and how with closes it
for you. Chapter 2 is next, about pulling text out of a file in more useful
shapes than one big string.