Binary Files and the Six Open Modes
Quick answer A binary file moves raw bytes instead of characters, so it needs a "b" mode — and rb, rb+, wb, wb+, ab, ab+ differ in exactly three things: whether the file must already exist, whether the old data survives, and where the position starts.
Every file on a disk is just a run of bytes. What changes is how Python interprets those bytes when you open it.
- Text mode (
"r","w","a") — Python decodes the bytes into astrfor you and, on Windows, also translates line endings. You work with characters. - Binary mode (
"rb","wb","ab") — Python hands you the bytes exactly as they sit on the disk, as abytesobject. No decoding, no line-ending translation.
Open the same file both ways and the difference shows up at once.
import os
f = open("marks.txt", "w")
f.write("Aarav,95\nDiya,88\n")
f.close()
f = open("marks.txt", "r")
print("TEXT mode :", repr(f.read()))
f.close()
f = open("marks.txt", "rb")
data = f.read()
print("BINARY mode:", data)
print("type :", type(data))
print("size on disk:", os.path.getsize("marks.txt"), "bytes")
f.close()
Real output, run on Windows:
TEXT mode : 'Aarav,95\nDiya,88\n'
BINARY mode: b'Aarav,95\r\nDiya,88\r\n'
type :
size on disk: 19 bytes
Compare the two carefully. In memory the text is 17 characters, but the file on disk is 19 bytes — text mode quietly turned each \n into \r\n. For a marksheet that is harmless. For a photo, an .exe, or a pickled record it is fatal: any byte that merely happened to be 0x0A would be silently rewritten and the data destroyed. That is the entire reason binary mode exists.
Binary mode then imposes two rules you will hit within five minutes of starting.
f = open("test.dat", "wb")
try:
f.write("Aarav")
except TypeError as e:
print("TypeError:", e)
f.write(b"Aarav")
f.close()
try:
f = open("nofile.dat", "rb")
except FileNotFoundError as e:
print("FileNotFoundError:", e)
f = open("test.dat", "rb")
print("read back:", f.read())
print("closed? ", f.closed)
f.close()
print("closed? ", f.closed)
TypeError: a bytes-like object is required, not 'str'
FileNotFoundError: [Errno 2] No such file or directory: 'nofile.dat'
read back: b'Aarav'
closed? False
closed? True
So: you write bytes, and you read bytes. Building those bytes by hand for a student record would be miserable, and that is precisely the gap the pickle module fills in the next section.
The six binary modes. Three questions separate all of them — must the file already exist, does the old data survive, and where does the position start?
| Mode | If file is missing | Existing data | Start position | Read | Write |
|---|---|---|---|---|---|
rb | FileNotFoundError | Kept | Byte 0 | Yes | No |
rb+ | FileNotFoundError | Kept | Byte 0 | Yes | Yes |
wb | Created | Erased | Byte 0 | No | Yes |
wb+ | Created | Erased | Byte 0 | Yes | Yes |
ab | Created | Kept | End of file | No | Yes, always at the end |
ab+ | Created | Kept | End of file | Yes | Yes, always at the end |
The + only ever adds a permission; it never protects your data. wb+ still wipes the file. The mode that catches students out is ab+, because its position starts at the end, not at 0.
import pickle
f = open("m.dat", "wb")
pickle.dump("AAA", f)
pickle.dump("BBB", f)
f.close()
f = open("m.dat", "ab+")
print("tell:", f.tell(), "| read() from here:", f.read())
f.seek(0)
print("after seek(0), load:", pickle.load(f))
pickle.dump("CCC", f)
f.close()
f = open("m.dat", "rb")
try:
while True:
print(" -", pickle.load(f))
except EOFError:
f.close()
tell: 36 | read() from here: b''
after seek(0), load: AAA
- AAA
- BBB
- CCC
Read both halves of that. read() returned b'' because the position was already at byte 36, the end of a 36-byte file. And even after seek(0), the new "CCC" still landed at the end. In append mode seek() repositions reading only — writes always go to the end. That is a guarantee of the operating system, not something Python can override.
One more split inside the append family: ab+ can read once you rewind, but plain ab cannot read at all.
f = open("ap.dat", "wb")
f.write(b"hello world")
f.close()
f = open("ap.dat", "ab")
print("ab -> tell:", f.tell())
try:
f.read()
except Exception as e:
print("ab -> read():", type(e).__name__, ":", e)
f.close()
f = open("ap.dat", "ab+")
print("ab+ -> tell:", f.tell(), " read():", f.read())
f.seek(0)
print("ab+ -> after seek(0), read():", f.read())
f.close()
ab -> tell: 11
ab -> read(): UnsupportedOperation : read
ab+ -> tell: 11 read(): b''
ab+ -> after seek(0), read(): b'hello world'
Both report tell() as 11, the size of the file, so neither starts at byte 0. But read() on the ab file raises io.UnsupportedOperation: read, while on ab+ it quietly returns b'' and only yields data after seek(0). That is what the No in the Read column means for ab — not "reads nothing", but "cannot read".
Finally, always close the file. close() flushes Python's buffer out to the disk; a program that ends without closing can leave records unwritten. The with statement closes for you, even if an exception is raised inside the block.
with open("m.dat", "wb") as f:
pickle.dump({"city": "Pune", "pin": 411001}, f)
print("closed after the with-block?", f.closed)
with open("m.dat", "rb") as f:
print(pickle.load(f))
closed after the with-block? True
{'city': 'Pune', 'pin': 411001}- Binary mode is chosen by putting a "b" in the mode string. It moves raw bytes, so no character decoding happens and no newline gets rewritten as a carriage-return pair.
- In binary mode f.write() takes bytes and f.read() returns bytes. Passing a str raises TypeError: a bytes-like object is required, not 'str'.
- rb and rb+ need the file to exist already and raise FileNotFoundError otherwise. wb, wb+, ab and ab+ all create the file if it is missing.
- wb and wb+ erase everything in the file the instant you open it. This is the single most common way students destroy their own data file.
- In ab and ab+ the position starts at the end of the file, and every write lands at the end no matter where you seek. Only ab+ can read: its read() returns b'' until you seek(0), while read() on a plain ab file raises io.UnsupportedOperation.
