Press m or Esc to close
Every program you have written so far forgets everything the moment it ends. This chapter makes data survive.
Data structures, functions, and a way to survive errors. All of it lives in RAM — and RAM is emptied when the program exits.
You write a marks program. It reads five students, computes the average, prints a report. You run it again tomorrow — and you have to type all five students in again.
A named area of secondary storage — a disk — that outlives the program. Write once, read on every future run, share it with another program entirely.
File — a named collection of related data stored on a secondary storage device, which persists after the program that created it has ended.
Reading a file gives you strings (Ch 1). Processing it needs loops and
lists (Ch 1). You will wrap it in functions (Ch 2). And a missing file
raises FileNotFoundError, so it all sits inside
try/except/finally (Ch 3).
.txtCharacters, human-readable, line by line. Openable in Notepad.
marks.txtRiya 88 CS
Aarav 91 CS
Kabir 76 IP
.datRaw bytes written by pickle. Not human-readable, but Python objects
come back exactly as they went in.
marks.dat — real bytes, as hex8004 9558 0000 0000 0000 005d
9428 5d94 288c 0452 6979 6194
4b58 8c02 4353 9465 5d94 288c
0541 6172 6176 944b 5b68 0365
.csvText, but with one record per line and fields separated by commas. Opens in Excel.
marks.csvName,Marks,Stream
Riya,88,CS
Aarav,91,CS
The same five students, stored three ways. Riya, Aarav, Kabir, Meera and Dev appear in every single slide from here on. Once you stop re-reading the data, you start seeing the difference between the three formats — which is the whole point.
input() and print() are file operations too — on the
standard input and standard output streams. Everything below is the
same idea, pointed at a disk.
read, readline,
readlines, and the loopwrite, writelines,
"w" vs "a"pickle, and search / append / updatecsv.reader, csv.writer,
newline=""| Operation | Text | Binary | CSV |
|---|---|---|---|
| Create / write | ✓ | ✓ | ✓ |
| Read | ✓ | ✓ | ✓ |
| Search | ✓ | ✓ | ✓ |
| Append | ✓ | ✓ | ✓ |
| Update | rewrite | ✓ | rewrite |
Questions are set as “write a function that …” — count, search, append, update. Learn the four shapes and every question is a variation.
first.pyf = open("marks.txt", "r") data = f.read() f.close() print(data) print("chars =", len(data))
The file ends with a newline after Dev 59 IP. read()
keeps it, and print adds its own — so you get one blank line.
That blank line is a mark.
① open — connect the program to the file and get a
file object.
② read or write, using that object's methods.
③ close — break the connection and flush anything pending.
| Mode | Means | If the file does not exist | If it does exist |
|---|---|---|---|
"r" | read — the default | FileNotFoundError | opens at the start |
"w" | write | creates it | erases everything in it |
"a" | append | creates it | opens at the end, keeps contents |
"r+" | read and write | FileNotFoundError | opens at the start |
"rb" "wb" "ab" | the same three, in binary | as above | as above |
f = open("marks.txt", "w") — on an existing file this empties it
instantly, before you have written a single byte. If you meant to add to the
file, you wanted "a". There is no undo.
read · write-from-scratch · append-to-the-end. The
b suffix only changes how the bytes are interpreted, never
where the file pointer starts.
"w" versus "a", demonstratedmodes.pyf = open("log.txt", "w") f.write("first\n") f.close() f = open("log.txt", "w") f.write("second\n") f.close() f = open("log.txt", "a") f.write("third\n") f.close() with open("log.txt") as f: print(f.read())
first is gone
The second open(..., "w") wiped the file before writing. Only the
"a" on the third open preserved what was already there.
"third\n" ends in a newline, and print adds another —
so the output really does end with an empty line. Write it in the exam.
close() matters — and withthe form the paper expectsf = open("marks.txt", "r") for line in f: print(line.strip()) f.close()
① Written data may sit in a buffer and never reach the disk.
② The operating system limit on open files is finite.
③ On some systems another program cannot open it.
with — closes automaticallywith open("marks.txt", "r") as f: for line in f: print(line.strip()) print("file closed?", f.closed)
with — opens the file and guarantees it is closed when
the block ends, even if an exception is raised inside. No
close() is needed.
with is better Python and NCERT teaches it. But most marking schemes
are written with open … close, and some questions say
"close the file" explicitly. Write with where you are free to, and
never forget close() when you are not.
WRONG — main.py1f = open("absent.txt", "r") 2print(f.read())
GUARDEDtry: f = open("absent.txt", "r") print(f.read()) except FileNotFoundError: print("absent.txt does not exist") finally: print("done")
Chapter 3 §3.2 promised this exact traceback. "r" on a missing
file is the single most common real exception in the syllabus — and the reason
except FileNotFoundError is worth a mark on its own.
IOError
FileNotFoundError is a subclass of OSError, which is the
same class the older name IOError refers to. Both are accepted;
FileNotFoundError is what Python actually prints, so prefer it.
safeclose.pyf = None try: f = open("marks.txt", "r") print(f.readline().strip()) print(int("abc")) except ValueError: print("bad number in the file") finally: if f is not None: f.close() print("closed:", f.closed)
finally ran even though the try block was abandoned
halfway. That is exactly what it is for — and file closing is the textbook
example the syllabus uses.
with
with open(...) does all of this for you, including the
if f is not None. Understand the long form, then prefer the short
one.
read() — the whole file as one stringwholefile.pyf = open("marks.txt", "r") data = f.read() f.close() print(type(data)) print(len(data)) print(data[:10])
read() — returns the entire remaining contents of the
file as a single string, newlines included.
read(n) — returns at most n characters.
read(n) — the pointer movesf = open("marks.txt", "r") print(f.read(4)) print(f.read(3)) f.close()
The second call did not start again from the beginning. It
continued from character 4 — and picked up the space before 88.
read() returns an empty string.pointer.pyf = open("marks.txt", "r") print(f.tell()) print(f.read(4)) print(f.tell()) f.seek(0) print(f.tell()) print(f.readline().strip()) f.close()
tell() — returns the current position of the file
pointer, in bytes from the start.
seek(n) — moves the file pointer to byte
n.
readline() and readlines()readline — one linef = open("marks.txt", "r") print(f.readline()) print(f.readline().strip()) f.close()
readline() keeps the \n at the end of the
line, and print adds another. The second call used
.strip() — and the blank line disappeared.
readlines — a list of linesf = open("marks.txt", "r") L = f.readlines() f.close() print(L) print(len(L), L[2].strip())
Quotes round every item, and a visible \n inside each one. Both
are marked.
| Method | Returns | Type | Use when |
|---|---|---|---|
read() | the whole file | one str | you want the text as a block |
read(n) | n characters | one str | you want a fixed chunk |
readline() | the next line | one str | one line at a time |
readlines() | every line | list of str | you need to index or count lines |
walk.pyf = open("marks.txt", "r") for line in f: print(line.strip()) f.close()
for line in f hands you one line at a time, in order, and stops at
the end of the file — with no counter, no readlines(), and
without loading the whole file into memory.
for line in f: — best, and memory-friendly.
for line in f.readlines(): — same result, whole file in RAM.
while True: line = f.readline() … if line == "": break
— the long form some question papers show.
.strip()
Every line except possibly the last carries a trailing \n.
Forgetting to strip it is why if line == "CS" mysteriously fails
and why your output is double-spaced.
count.pylines = 0 words = 0 chars = 0 with open("marks.txt", "r") as f: for line in f: lines = lines + 1 words = words + len(line.split()) chars = chars + len(line) print("Lines:", lines) print("Words:", words) print("Chars:", chars)
len(line) counts the \n too — five of them. If the
question says "excluding newlines", use
len(line.strip()) and the answer becomes 52.
Read which one is asked.
split() with no argument
It splits on any run of whitespace and discards empties — so it handles double spaces and the trailing newline correctly. That is why it is the right tool for counting words.
search.pycount = 0 with open("marks.txt", "r") as f: for line in f: if "CS" in line: count = count + 1 print(line.strip()) print("CS students:", count)
① a counter at 0 ② loop over the lines
③ an if that tests the line ④ act and count
⑤ report after the loop.
Only step ③ changes between questions.
splitting into fieldstotal = 0 n = 0 with open("marks.txt", "r") as f: for line in f: parts = line.split() total = total + int(parts[1]) n = n + 1 print(parts) print("Average =", round(total / n, 2))
parts holds the last line's fields after the loop — a local
name outliving the loop, exactly as in Chapter 1.
And parts[1] is the string '59', so
int() is compulsory. Add strings and you get
'8891...'.
write() and writelines()writeone.pyf = open("out.txt", "w") f.write("Hello\n") f.write("Class XII\n") f.close() f = open("out.txt", "r") print(f.read()) f.close()
write adds no newline
Unlike print, write() writes exactly what you
give it. Leave out the \n and the whole file becomes one line:
HelloClass XII.
writemany.pylines = ["one\n", "two\n", "three\n"] f = open("nums.txt", "w") f.writelines(lines) f.close() with open("nums.txt") as f: print(f.read())
write(s) — writes the string s to the file.
writelines(L) — writes every string in the list
L. It adds no separators either — the \ns must
already be in the strings.
f.write(88) raises
TypeError: write() argument must be str, not int. Convert first:
f.write(str(88)). This is the writing-side twin of needing
int() when you read.
You wrote 88, an int. You read back
'88', a str. You wrote a list of five records; you
read back one long string you have to split() apart again by
hand.
Put a Python object — a dictionary, a list of lists — into a file, and get the same object back, types intact, with no parsing.
Binary file — a file that stores data as raw bytes rather than as readable characters. It cannot be opened meaningfully in a text editor.
Pickling — converting a Python object into a stream of bytes for storage. Unpickling is the reverse.
Serialisation and deserialisation. Both words appear in question papers; they mean pickling and unpickling.
You gain exact types and one-line storage of any structure. You lose
readability — nobody, including you, can inspect a .dat file without
a Python program.
dump() and load()onerecord.pyimport pickle student = {"name": "Riya", "marks": 88, "stream": "CS"} f = open("one.dat", "wb") pickle.dump(student, f) f.close() f = open("one.dat", "rb") back = pickle.load(f) f.close() print(back) print(type(back), back["marks"])
pickle.dump(obj, f) — writes obj to the open
binary file f. Note the order: object first, file second.
pickle.load(f) — reads one object back from f
and returns it.
back["marks"] is the int 88 — not '88'. No
split(), no int(). That is the entire selling point
of pickle.
"wb" / "rb", always
import pickle at the top, and never a plain "w" or
"r".
Because pickle produces bytes, not characters. Text mode wraps
the file in an encoder that assumes every byte is part of a valid UTF-8 character —
and pickle's very first byte, 0x80, is not.
f = open("marks.dat", "r")data = pickle.load(f)UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0:
invalid start byte
Text mode also silently translates newline bytes on Windows, which would corrupt
any pickled data containing the byte 0x0d. Both problems vanish in
binary mode, because binary mode does no interpretation at all — it moves bytes
exactly as they are.
writebin.pyimport pickle records = [["Riya", 88, "CS"], ["Aarav", 91, "CS"], ["Kabir", 76, "IP"], ["Meera", 94, "CS"], ["Dev", 59, "IP"]] with open("marks.dat", "wb") as f: pickle.dump(records, f) with open("marks.dat", "rb") as f: data = pickle.load(f) print(data) print(len(data))
The whole list was pickled as a single object, so a single
load() brings it all back. This is by far the easiest pattern to
get right, and it is what the marking scheme shows.
You can call dump() once per record and then load()
repeatedly in a while True loop until EOFError is
raised. It works, and it is the reason EOFError is in the
Chapter 3 table — but the single-list version is simpler and safer.
searchbin.pyimport pickle with open("marks.dat", "rb") as f: data = pickle.load(f) for rec in data: if rec[1] > 85: print(rec[0], rec[1])
In §3.6 you had to split() the line and int() the
field before comparing. Here rec[1] is already an int, so
> 85 just works.
① open "rb" ② load() the whole
structure ③ loop over it in memory ④ test each
record.
There is no line-by-line reading — you get the objects back and search them
like any list.
updatebin.pyimport pickle with open("marks.dat", "rb") as f: data = pickle.load(f) for rec in data: if rec[0] == "Kabir": rec[1] = rec[1] + 5 with open("marks.dat", "wb") as f: pickle.dump(data, f) with open("marks.dat", "rb") as f: print(pickle.load(f))
① load() everything into a list.
② change it in memory — this is Chapter 1 mutability doing the
work.
③ reopen in "wb" and dump() the whole thing
back.
rec[1] = … is enough
rec is a label on a list inside data — the
same object, not a copy. Mutating it changes data. That is
Chapter 1 §6.6 and Chapter 2 §7.2, and it is why no re-assignment into
data is needed.
Two programs. marks.dat is a valid pickled file;
empty.dat is a zero-byte file. Name the exception each raises.
1import pickle 2f = open("marks.dat", "r") 3data = pickle.load(f)
The mode is wrong. Text mode tried to decode pickle's raw bytes as UTF-8 characters and choked on the very first one.
1import pickle 2f = open("empty.dat", "rb") 3data = pickle.load(f)
The mode is right this time — there is simply nothing in the file to unpickle.
Both crash on pickle.load(f), and the traceback looks almost
identical. The exception name is the only thing that tells you whether to
fix the open() mode or to check that the file was written at all.
CSV — Comma Separated Values. A text file in which each line is one record and the values within a record are separated by commas.
A text file is readable but unstructured. A binary file is structured but unreadable. CSV is both — a human can open it in Notepad, and Excel, Google Sheets and every database in the world can import it.
marks.csvName,Marks,Stream
Riya,88,CS
Aarav,91,CS
Kabir,76,IP
Meera,94,CS
Dev,59,IP
Name,Marks,Stream is the header row — field names, not
data. It is optional, but almost every file has one, and almost every exam
question requires you to skip it.
split(",")
And for a file this simple it would work. The csv module exists
because real data contains commas inside fields —
"Sharma, Riya",88,CS — and quotes, and embedded newlines.
csv.reader handles all of it; split(",") silently gets it
wrong.
writecsv.pyimport csv rows = [["Name", "Marks", "Stream"], ["Riya", 88, "CS"], ["Aarav", 91, "CS"], ["Kabir", 76, "IP"], ["Meera", 94, "CS"], ["Dev", 59, "IP"]] f = open("marks.csv", "w", newline="") w = csv.writer(f) w.writerows(rows) f.close() print(open("marks.csv").read())
csv.writer(f) — creates a writer object bound to the
open file f.
writerow(list) — writes one record.
writerows(list of lists) — writes many.
88 was written as the characters 8 and
8. A CSV is a text file — it has no idea what a number is. That
is why reading it back gives you strings.
readcsv.pyimport csv with open("marks.csv", "r", newline="") as f: r = csv.reader(f) for row in r: print(row)
row? 🤔'88', with quotes — not 88. Any arithmetic needs
int(row[1]) first. This is the single most common CSV mistake, and
it is the same lesson as input() in Chapter 1 §2.8.
csv.reader(f) — returns an object you iterate over. Each iteration yields one record as a list of strings.
filtercsv.pyimport csv with open("marks.csv", "r", newline="") as f: r = csv.reader(f) header = next(r) print("header:", header) for row in r: if int(row[1]) > 85: print(row[0], row[1])
next(r) pulls one record off the reader and moves on, so
the for loop starts at the first real record.
int(row[1]) is compulsory — without it you would be comparing the
string '88' with the int 85, which raises
TypeError.
for row in r: then if row[0] == "Name": continue
also works and is what some question papers show. next() is
cleaner and reads better.
newline=""appendcsv.pyimport csv f = open("marks.csv", "a", newline="") w = csv.writer(f) w.writerow(["Ishan", 67, "IP"]) f.close() print(open("marks.csv").read())
newline=""
The csv module writes its own record terminator. In text mode
Python also translates \n to the platform's line ending.
newline="" switches that translation off so the two do not
collide.
On every open() that a csv.reader or
csv.writer will touch — read and write. It is a
one-mark habit and it costs nothing.
newline="" out?
It depends on the operating system, which is exactly why the rule is stated as absolute.
\n into \r\n. The
csv writer has already ended the record with \r\n, so the file gets
\r\r\n and every second line comes out blank. This is the
famous symptom.So a student on Linux may never see the bug, ship the habit, and lose the mark —
or produce a broken file for a teacher on Windows. The syllabus requires
newline="", and it is correct on every platform, which is the
whole reason to write it unconditionally rather than "when it breaks".
Reading matters too, for a different reason: without it, a field containing an embedded newline is split across two records.
Text .txt | Binary .dat | CSV .csv | |
|---|---|---|---|
| Human-readable | Yes | No | Yes |
| Open mode | "r" "w" "a" | "rb" "wb" | "r" + newline="" |
| Module needed | none | pickle | csv |
| What you read back | str | the original object | list of str |
| Types preserved | No | Yes | No |
| Read methods | read readline readlines | pickle.load | csv.reader |
| Write methods | write writelines | pickle.dump | writerow writerows |
| Other software can read it | Yes | No | Yes — Excel |
marks.txt contains the five lines shown earlier. Write the output.
with open("marks.txt", "r") as f: lines = f.readlines() print(lines[0].strip()) print(lines[-1].strip()) print(len(lines)) print(lines[1][:5])
readlines() gives a list, so all of Chapter 1's list operations
apply — [0], [-1], len(), and slicing the
string inside. lines[1][:5] is the first five characters of
'Aarav 91 CS\n'.
Write a function count_vowel_lines() that reads marks.txt
and displays those lines that begin with a vowel, then returns how many
there were.
vowellines.pydef count_vowel_lines(): n = 0 with open("marks.txt", "r") as f: for line in f: if line[0] in "AEIOUaeiou": n = n + 1 print(line.strip()) return n print("Count:", count_vowel_lines())
① opening in "r" and closing (or using
with).
② testing line[0] against both cases — a question
that says "a vowel" means either case unless it says otherwise.
③ return n, not print(n) — the question said
"returns".
line[0] on an empty line raises
IndexError. If a file may contain blank lines, test
if line.strip() and line[0] in ….
marks.dat holds a pickled list of [name, marks, stream]
records. Write a function topper() that returns the record with the
highest marks.
topper.pyimport pickle def topper(): with open("marks.dat", "rb") as f: data = pickle.load(f) best = data[0] for rec in data: if rec[1] > best[1]: best = rec return best print(topper())
① import pickle and mode "rb".
② pickle.load(f) into a variable.
③ the max-finding loop, seeded with data[0] rather
than 0 — a record, not a number.
No split(), no int(). rec[1] came back
as an int because pickle preserved the type.
marks.csv has a header row and then the five records. Write a function
stream_count() that returns a dictionary giving how many students are
in each stream.
streams.pyimport csv def stream_count(): counts = {} with open("marks.csv", "r", newline="") as f: r = csv.reader(f) next(r) for row in r: s = row[2] counts[s] = counts.get(s, 0) + 1 return counts print(stream_count())
① newline="" on the open.
② next(r) to discard the header — without it you would
count a phantom 'Stream' student.
③ the counts.get(s, 0) + 1 idiom from Chapter 1 §8.5.
Exactly the banana program, pointed at a file instead of a
string. That is what this whole chapter is — old algorithms, new source
of data.
readline() and
readlines()? What does each return?A text file stores data as readable characters, organised into lines
terminated by a newline. It is opened in modes such as "r" and
"w", and everything read from it is a string.
A binary file stores data as raw bytes with no character interpretation. It
is opened in "rb" / "wb" and, using
pickle, returns the original Python objects with their types
intact.
Advantage of text: it can be read and edited by a human in any editor, and shared with other software. Advantage of binary: data types are preserved, so no parsing or conversion is needed, and it is faster and more compact.
readline() reads one line from the current position and returns
it as a string, including its trailing newline. At end of file it returns an
empty string.
readlines() reads all the remaining lines and returns them as a
list of strings, each still carrying its newline.
marks.txt holds the five records. What is printed, and what is in the
file afterwards?
f = open("marks.txt", "r") print(f.readline().strip()) print(len(f.readlines())) print(f.read()) print("[" + f.read() + "]") f.close()
Line 1 — readline() took the first record and left the pointer
at the start of line 2.
Line 2 — readlines() took the remaining four, so the
length is 4, not 5. The pointer is now at the end of the file.
Lines 3 and 4 — every further read() returns the empty
string, because there is nothing left. print("") emits a blank
line, and the brackets on the last line prove the string really is empty.
The file is unchanged. It was opened "r"; reading never
modifies. To restart you would need f.seek(0).
You can now store and retrieve data. Chapter 5 is about the order in which you take it back out — the last-in-first-out discipline that runs undo buttons, browser back, and Python's own function calls.
① A stack is built on a list — append and
pop, which you have used all chapter.
② The call stack in a traceback is a real stack; Chapter 5 explains
why it is drawn innermost-last.
③ The classic stack question reads records from a file and pushes them —
so §5 and §6 of this chapter are the input to the next one.