Intuition The one core idea
A file is nothing but a long line of numbered boxes on disk, and a file object is a single finger pointing at one of those boxes. Everything in this whole topic — the mode (the little code you pick when opening, defined in §5), the moving finger (the cursor, defined in §4), read, write — is just choosing which direction the finger moves , what the boxes contain (letters or raw numbers) , and where the finger starts .
Before you can trust a single line of the parent note, you need to know what every word and symbol in it actually means and what picture it stands for. This page builds them one at a time, each leaning on the one before. If a term appears in the parent, it is defined here first.
Definition A file = a numbered strip of boxes
A file on disk is a finite sequence of boxes , numbered 0 , 1 , 2 , 3 , … from the left. Each box holds ONE small number between 0 and 255 . That number is called a byte . Nothing else lives in a file — no "lines", no "words", no colours. Just boxes of numbers in a row.
Look at the strip below. The numbers under each box are its position (we call this the offset — how far from the start). The numbers inside the boxes are the bytes.
Why we need this picture: every later idea — "finger position", "read", "EOF", "seek" — is a statement about positions on this strip . If you can see the strip, the rest is easy.
Intuition Why "byte" and why 0–255?
A byte is 8 on/off switches. Eight switches make 2 8 = 256 combinations, i.e. the numbers 0 through 255 . That is the smallest chunk a disk stores, so it is the atom of every file.
The strip only holds numbers, but you want to store the word apple. So we need an agreed table that says "the letter a shall be stored as the number 97, b as 98, …".
An encoding is a lookup table turning characters (letters, digits, emoji) into bytes, and back again. The one Python uses by default is called UTF-8 . "Encode" = letter → number(s). "Decode" = number(s) → letter.
apple on the strip
The five letters a p p l e become the five bytes 97 112 112 108 101. When you read as text , Python looks each number up in the UTF-8 table and hands you back the letters. When you read as binary , it hands you the raw numbers and stays silent about their meaning.
This is exactly why the topic distinguishes text mode (numbers → letters for you, using an encoding) from binary mode (raw numbers, no translation). See Strings and Encoding (UTF-8) and Bytes vs str for the full table.
Common mistake "One letter is always one byte."
Why it feels right: for a–z in UTF-8 it is one byte. Reality: an emoji or accented letter can be 2–4 bytes. That is why UTF-8 is called variable-length . The topic mostly uses plain letters so 1 byte = 1 char there, but never assume it in general.
str and bytes
A ==str== is a sequence of characters — human text. Python prints it as 'apple'.
A ==bytes== is a sequence of raw numbers 0 –255 . Python prints it as b'apple' (note the leading b), showing readable bytes as letters and the rest as \xNN.
Intuition Why the leading
b decides everything
'apple' and b'apple' are stored identically on the strip (same five boxes). The b is not part of the data — it is a label on the container telling Python how to treat those boxes. Without b, Python promises "I will run these boxes through UTF-8 and give you letters". With b, Python promises "I will hand you the raw numbers untouched". Same boxes, two different promises. That single letter is why one read gives you str and another gives you bytes.
When the parent writes f.read(8) giving b'\x89PNG\r\n\x1a\n', the b tells you: these are raw bytes, do not read them as words. The \x89 means "the single byte whose number is 0 x 89 = 137 ".
\n — the newline byte
\n is one single character (the byte 10 ) that means "start a new line here". It is not a slash followed by an n; the backslash is Python's way of writing an invisible character. On screen it shows as a line break.
Why the topic obsesses over \n: the whole difference between read, readline, and readlines is where they stop relative to the \n boxes . readline() stops right after the next \n; readlines() splits the strip at every \n. You cannot understand those three methods without seeing where the \n boxes sit.
But here is a real-world twist you must know:
Definition LF vs CRLF — two ways disks mark a line break
Different operating systems historically ended lines differently:
LF = just \n (byte 10 ). Used by Linux and macOS.
CRLF = \r\n (two bytes: 13 then 10 ). Used by Windows. \r is "carriage return", the byte 13 .
So the same text file created on Windows literally has an extra byte (13 ) before every line break on the strip.
Intuition Universal newlines — why you rarely notice CRLF
Python's text mode does you a favour called universal newline translation :
On reading , any \r\n (or lone \r) on the strip is quietly turned into a single \n before it reaches you . Your code always sees clean \n.
On writing , a \n you write is translated back to the platform's native ending (\r\n on Windows, \n on Linux) as it lands on the strip .
So in text mode you think in \n and the messy \r is handled invisibly.
rb gives me the same lines as r."
Why it feels right: it's the same file. Reality: binary mode does NO newline translation . Open a Windows file with rb and you will see the raw \r\n — byte 13 then 10 — sitting in your data. That surprise \r at the end of lines is the classic bug when people switch to binary. Fix: stay in text mode for text, or strip \r yourself in binary.
\n counts as two characters."
Why it feels right: you typed two keys, \ and n. Reality: it is stored as one byte (10 ). len("a\n") is 2 , not 3 . (But "a\r\n" is 3 : a, \r, \n.)
The cursor is a single number: which box the finger is pointing at right now . It starts somewhere (the mode decides where), and every read or write moves it forward by however many boxes were consumed.
This is the single fact that explains every "surprise" in the parent:
Why a second f.read() returns '' → the finger is already past the last box (at EOF ).
Why readline() twice gives line 1 then line 2 → the finger remembers where it stopped.
Why f.seek(0) re-reads → it drags the finger back to box 0 .
Definition EOF — end of file
EOF ("end of file") is the position just past the last box . A read that starts at EOF has nothing left to give, so it returns the empty result ('' for text, b'' for bytes).
f.seek(offset) — move the finger by hand
seek sets the cursor to a chosen box number directly. f.seek(0) jumps to the start; f.seek(3) jumps to box 3 .
A mode is the tiny string you pass to open() that answers, all at once: read or write? keep or wipe? start or end? letters or raw bytes? Now every ingredient has a picture — see the strip figure below the table:
Symbol
Plain meaning
Picture on the strip
r
read only
finger at box 0 , can only pull bytes out; file must exist
w
write, wipe first
strip emptied to length 0 , finger at box 0
a
append
finger jumps to EOF, can only push bytes on the end
x
exclusive create
like w, but refuses (error) if the file already exists
t
text (default)
boxes run through UTF-8 → you get str; newlines translated
b
binary
boxes handed over as raw numbers → bytes; no translation
+
read AND write
finger can both pull and push
t is the silent default
You almost never type t. Writing open(p, "r") is exactly open(p, "rt") — text is assumed. You add b only when you want to switch that default off. So a mode string is really [base letter][t or b][optional +], with t invisible unless you say b.
x exists alongside w
w will happily overwrite an existing file — sometimes that silently destroys someone's data. x is the safe cousin: "create this file, but only if it does not already exist; otherwise stop and tell me." Use x when accidentally clobbering a file would be a disaster.
Intuition Why intent must be declared
up front
The operating system prepares the strip differently for each intent: w must erase it immediately, a must find the end immediately, r must refuse if the strip doesn't exist, x must refuse if it does exist. So you hand it one tiny code string before any data moves. That is all a "mode" is.
with open(...) as f:
A context manager is a helper that guarantees cleanup happens — here, that the file is closed (its buffer flushed to disk) even if your code crashes midway. with is the polite friend who shuts the notebook for you. Full story in Context Managers (with statement) .
Intuition Why closing matters
Writes are often held in a temporary buffer in RAM and only pushed to disk when you close. Forget to close → the data may never land on the strip. with removes that risk entirely.
Errors like "file not found" (from r on a missing file) or "file already exists" (from x) are handled with Exceptions try-except , and building the path string safely ("folder/notes.txt") is the job of os and pathlib .
File = strip of numbered boxes
Encoding UTF-8 turns letters into bytes
str vs bytes via b prefix
Newline LF byte 10 vs CRLF
Text mode translates newlines
seek moves finger by hand
read readline readlines write
with block closes and flushes
Read it top-down: the strip gives us bytes, bytes give us encoding and thus str/bytes, the newline (and its CRLF/LF flavours) lets us talk about lines, the cursor and seek let us talk about where , and all of these feed the modes and read/write methods — which the with block finally makes safe.
Test yourself — reveal only after you have answered aloud.
A file on disk physically is… a finite, ordered sequence of numbered boxes, each holding one byte (a number 0–255).
A byte can hold which range of values, and why that range? 0 to 255, because 8 on/off switches give 2 8 = 256 combinations.
What does an encoding like UTF-8 do? Maps characters to bytes (encode) and bytes back to characters (decode).
How do you tell str from bytes when Python prints them, and what does the b really label? bytes has a leading b (e.g. b'apple'); str does not. The b labels the container's promise (raw numbers vs UTF-8 letters), not the stored boxes.
\n is how many characters, and what byte number?One character, byte number 10 (LF).
What is CRLF and where does it appear? \r\n — two bytes, 13 then 10 — the Windows line ending; text mode translates it to a single \n on read.
What does binary mode do about newline translation? Nothing — rb/wb give you the raw bytes including any \r (13).
What is the cursor, and what moves it? A number saying which box the finger points at; every read or write advances it, and seek moves it by hand.
What happens on seek to a negative offset, and past EOF? Negative → error (no box before 0). Past EOF → allowed; reads give empty, writing there leaves a zero-filled gap.
What is EOF and what does a read at EOF return? The position just past the last box; returns '' (text) or b'' (binary).
What do modes x and t mean? x = exclusive create (error if the file already exists); t = text mode, the invisible default (gives str, translates newlines).
What three things does a mode string decide (plus text/binary)? Direction (read/write), keep or wipe/create, starting position (start or end); plus text vs binary via b (default t).
Why must you declare intent (the mode) before moving data? The OS prepares the strip differently — erase for w, seek end for a, refuse-if-missing for r, refuse-if-exists for x — before any byte moves.
What guarantees a file is closed and its buffer flushed even on error? The with open(...) as f: context manager.