PYTHON / FILE HANDLING
Opening files and choosing a file mode
Choose the right open() mode for a task and predict whether the call creates, truncates, or refuses a file, and whether reads give str or bytes.
What you will learn
- Pick among r, w, a, x by asking: must the file exist, and must its contents survive
- Add + for the opposite direction, but know a+ still writes only at the end
- Add b to work in bytes; encoding= and newline= are text-mode-only arguments
- Expect FileNotFoundError from r and FileExistsError from x at the open() call itself
Understanding Opening files and choosing a file mode
open(path, mode) does not hand you the contents of a file; it hands you a stream object positioned somewhere in that file, and the mode string decides how that positioning is set up. The first letter answers three questions at once: must the file already exist, does its current content survive the call, and where does the cursor start. "r" demands an existing file and starts at byte 0; "w" creates or empties, always starting at byte 0 of an empty file; "a" creates if needed and pins writes to the end; "x" creates and raises FileExistsError if the name is taken.
The rest of the mode string is made of independent modifiers, not more choices of the same kind. "+" adds the direction you did not ask for, so "r+" is read-and-write on an existing file with nothing destroyed, while "w+" is read-and-write on a file that was just emptied. "b" removes the text layer: in text mode open() wraps a buffered byte stream in a decoder and a newline translator, which is why read() gives you str and why encoding= and newline= exist; in binary mode there is no decoder, read() gives bytes, and passing encoding= is a ValueError.
Because the mode is enforced by the operating system at open time, it is a correctness tool and not a formality. Truncation under "w" happens the instant open() returns, before your first write, so a crash later still leaves you with an empty file. "x" performs the exists-check and the create as one indivisible operation, which is why it is safer than testing with os.path.exists and then opening "w": no other process can slip in between. If you ever need to know what a stream actually permits, ask the object: f.mode, f.readable(), f.writable(), f.seekable().
Modes also determine which exceptions you must plan for, so the try/except belongs around the open() call rather than around the reads and writes that follow.
import os
path = "notes.txt"
if os.path.exists(path):
os.remove(path)
# "x" creates a new file and refuses to touch an existing one
f = open(path, "x", encoding="utf-8")
print(f.mode, f.writable(), f.readable())
f.write("first line\n")
f.close()
try:
open(path, "x", encoding="utf-8")
except FileExistsError:
print("x refused an existing file")
# "w" opens the same name and empties it immediately
f = open(path, "w", encoding="utf-8")
print("size right after open with w:", os.path.getsize(path))
f.write("replaced\n")
f.close()
# "r" is the default and cannot write
f = open(path, encoding="utf-8")
print(f.mode, repr(f.read()))
f.close()
try:
open("no_such_file.txt", encoding="utf-8")
except FileNotFoundError as e:
print(type(e).__name__, e.errno)
os.remove(path)The mode string decides, before a single byte moves, whether the file must exist, whether its old contents survive, and whether you get str or bytes.
Worked examples
r+ overwrites in place, a+ always appends
Shows that the + modes differ in where the cursor starts and, for append, where writes are forced to land.
import os
with open("demo.txt", "w", encoding="utf-8") as f:
f.write("abcdefgh")
f = open("demo.txt", "r+", encoding="utf-8")
print(f.tell(), f.read(3))
f.seek(0)
f.write("XY")
f.close()
with open("demo.txt", encoding="utf-8") as g:
print(g.read())
f = open("demo.txt", "a+", encoding="utf-8")
print(f.tell())
f.seek(0)
f.write("Z")
f.close()
with open("demo.txt", encoding="utf-8") as g:
print(g.read())
os.remove("demo.txt")Example explained
Line 1"r+" requires the file to exist and truncates nothing, so tell() is 0 and read(3) returns the original "abc".
Line 2After seek(0), write("XY") replaces two characters in place; nothing is shifted, so the length stays 8.
Line 3"a+" opens with the cursor already at the end, which is why tell() prints 8 instead of 0.
Line 4Append mode is enforced below Python by the O_APPEND flag, so seek(0) is ignored for writing and "Z" still lands at the end.
The b flag turns off decoding and newline translation
Compares the same file read in text mode and binary mode, and shows why encoding= is rejected with a binary mode.
import os
path = "line.txt"
with open(path, "w", encoding="utf-8", newline="\r\n") as f:
f.write("héllo\n")
with open(path, "r", encoding="utf-8") as f:
print(repr(f.read()))
with open(path, "rb") as f:
raw = f.read()
print(type(raw).__name__, raw, len(raw))
try:
open(path, "rb", encoding="utf-8")
except ValueError as e:
print("ValueError:", e)
os.remove(path)Example explained
Line 1newline="\r\n" makes text mode translate the single "\n" I wrote into two bytes on disk.
Line 2Reading back in text mode applies universal newlines, collapsing "\r\n" to "\n", so repr shows one escape.
Line 3"rb" skips both the decoder and the newline translator, so read() returns 8 raw bytes, including the \r and the two-byte UTF-8 é.
Line 4encoding= only exists on the text layer, so combining it with "rb" is rejected before the file is even touched.
Important notes
In current CPython the default encoding for text mode comes from the platform locale, not from the file, so a file that reads fine on your machine can raise UnicodeDecodeError on someone else's; state encoding explicitly.
"t" is the default, so "r" and "rt" are identical, and "+" is never a mode on its own: it only modifies r, w, a, or x.
Common mistakes
Using "w" to add a line to an existing file: the file is emptied the moment open() returns, so the old content is gone before any write happens.
Expecting "r+" writes to insert text: they overwrite character for character at the current position, so two characters written over "abcdefgh" give "XYcdefgh", not a longer file.
Checking os.path.exists(path) and then opening with "w" to avoid clobbering: another process can create the file in between, whereas "x" makes the check and the create a single atomic step.
Try it yourself
Change, predict, then run
Write a save(path, text) function that opens with mode "x" and prints "refusing to overwrite" instead of writing when the file already exists, then call it twice with different text and read the file back to confirm the first version survived.
Open the Python workspaceCheck your understanding
A script runs f = open("log.txt", "a+", encoding="utf-8") on a file that already contains 40 characters, then calls f.seek(0) and f.write("start"). Where does "start" end up?
- At the end of the file, because append mode forces every write to the end no matter what the position is
- Over the first five characters, because seek(0) moved the write position to the beginning
- Inserted at the beginning, pushing the existing 40 characters forward
- Nowhere: the write raises io.UnsupportedOperation because seeking makes an a+ stream read-only
Show answer
Append mode is enforced by the operating system's O_APPEND flag, which relocates each write to the current end of file, so seek(0) only affects reading. Option 2 describes "r+", the mode that really does overwrite at the seeked position; no Python file mode ever inserts and shifts existing bytes.