Topic 7.5
Reading and Writing Text Files
In one line
open() connects your program to a file. The mode says whether you read, write or append, the encoding says how characters become bytes (always pass encoding="utf-8"), and iterating over the file reads one line at a time without loading it all.
Think of it like this
A notebook on a desk. To read it you open it at the first page and go forward. To write with a fresh start you tear out all the old pages first. To append you open it at the end and add new lines. Which of these you intend must be said before you touch the notebook. In Python the notebook is a file, and the thing you say first is the mode.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- File object
- What open() returns: a handle you use to read from or write to a file.
- Mode
- A short string saying what you plan to do: r read, w write (erases), a append, x create new, b binary.
- Encoding
- The rule for converting characters to bytes and back. UTF-8 is the standard choice.
- Bytes
- Raw numbers from 0 to 255. All files are made of bytes; text is bytes plus an encoding.
- Buffer
- A holding area in memory. Writes collect there and reach the disk later, on flush or close.
- Newline
- The invisible character that ends a line. Written as \n in Python.
- Lazy reading
- Reading a little at a time as needed, instead of loading everything.
- Mojibake
- Garbled text like é that appears when text is decoded with the wrong encoding.
Step by step
01Write a file
Mode "w" creates the file or empties it. write takes a string and does not add a line break, so we end each line with \n ourselves. Everything is inside with (Topic 7.4), so the file is closed (and the data saved) at the end of the block.
with open("shopping.txt", "w", encoding="utf-8") as f:
f.write("milk\n")
f.write("bread\n")
print("eggs", file=f) # print adds the newline02Append without erasing
Mode "a" keeps what is there and adds at the end. Opening with "w" again would have deleted the three lines, which is a classic accident.
with open("shopping.txt", "a", encoding="utf-8") as f:
f.write("rice\n")03Read it back in three ways
read() gives one big string. readlines() gives a list of lines (each with its \n). Iterating gives one line at a time. For anything that might be big, iterate.
with open("shopping.txt", encoding="utf-8") as f:
everything = f.read() # one string
with open("shopping.txt", encoding="utf-8") as f:
for line in f: # one line at a time
print(line.rstrip("\n"))04Text is bytes plus an encoding
.encode("utf-8") turns text into bytes, and .decode("utf-8") turns bytes back into text. A file opened in text mode does this for you. The word café is 4 characters but 5 bytes, because é needs two bytes in UTF-8. The b'...' form is how Python shows bytes, and \xc3\xa9 means the two byte values 195 and 169 written in hexadecimal (base 16).
data = "café".encode("utf-8")
print(len("café"), len(data), data) # 4 5 b'caf\xc3\xa9'
print(data.decode("utf-8")) # café
print(data.decode("latin-1")) # café (wrong encoding: mojibake)05Newlines are translated for you
In text mode a Windows-style line ending in the file is read as plain \n. Pass newline="" to see the raw endings. For ordinary text files leave the default.
with open("win.txt", "w", encoding="utf-8", newline="") as f:
f.write("a\r\nb\r\n") # write raw Windows endings
with open("win.txt", encoding="utf-8") as f:
print(repr(f.read())) # 'a\nb\n'
with open("win.txt", encoding="utf-8", newline="") as f:
print(repr(f.read())) # 'a\r\nb\r\n'06Big files: never read() what you can iterate
This counts the ERROR lines in a log file (a file where a program writes down what happened; the next example builds one so you can run it). for line in f keeps only the current line in memory. You can stop early with break when you have what you need. Compare this with f.read().splitlines(), which builds the whole file as one string and then a list of all the lines as well.
count = 0
with open("big.log", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
count += 1
print(count)Try it yourself
- 1
Overwrite by accident
In the first example change the second mode to "w" and run it. Which lines are left? Change it back to "a" and note the difference. Then try mode "x" on the very first open and run it twice. What does the second run say?
- 2
Count bytes
In the second example change the text to
"hello", then to"नमस्ते". Compare the characters and bytes counts. How many bytes does one Hindi letter take in UTF-8? - 3
Find the line
In the last example make the program print the line number (starting from 1) of the second ERROR line. Hint: use
enumerate(f, start=1).
Code & diagrams
Change the second open from "a" to "w" and see what happens to the first lines.
Expected output
1 milk
2 bread
3 eggs
4 rice
['milk\n', 'bread\n', 'eggs\n', 'rice\n']The same bytes give different text under different encodings. That is why you always state the encoding.
Expected output
characters: 6 bytes: 9
bytes: b'caf\xc3\xa9 \xe2\x82\xb9'
decoded right: café ₹
decoded wrong: café â¹
file holds: b'caf\xc3\xa9 \xe2\x82\xb9'Expected output
default : 'a\nb\n'
raw : 'a\r\nb\r\n'
decode error: unexpected end of data at byte 3
with errors=replace: caf�Imagine 10 million lines instead of 2000: the loop still holds only one line at a time and stops at the first match.
Expected output
errors: 4
first: 0499 ERROR request handled
['0000 INFO request handled', '0001 INFO request handled', '0002 INFO request handled']Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Open a file that does not exist
Read a file that was never created.
with open("missing.txt", encoding="utf-8") as f:
print(f.read())Break #2
Read bytes with the wrong encoding
Save text as Latin-1 and read it as UTF-8.
with open("old.txt", "wb") as f:
f.write("café".encode("latin-1"))
with open("old.txt", encoding="utf-8") as f:
print(f.read())Myth vs fact
Myth
encoding is optional because Python works it out.
Fact
Python does not detect the encoding. It uses a default that depends on the system (until UTF-8 became the default in 3.15), so the same code can read a file correctly on one machine and fail on another. State it.
Myth
f.write(42) writes the number 42.
Fact
write only takes strings and raises TypeError for anything else. Write str(42) or an f-string.
Myth
Mode "w" adds to the file.
Fact
"w" truncates (empties) the file the moment you open it, before you write anything. Use "a" to add.
Myth
Reading the whole file with read() is fine because computers have lots of memory.
Fact
A log or dataset can be larger than memory. Iterate by line (or by chunk) unless you know the file is small.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
File objects are layered: a raw binary stream, wrapped by a
BufferedReader/BufferedWriter, wrapped by aTextIOWrapperthat does the decoding and newline translation.open(..., "rb")returns the buffered layer without the text wrapper. - ▸
Writes are buffered, so data can be lost on a crash until the buffer is flushed.
f.flush()pushes it to the operating system, andos.fsync(f.fileno())asks the system to write it to disk. For safe replacement of a file, write to a temporary file in the same folder and thenos.replaceit over the target. - ▸
Use
encoding="utf-8-sig"when reading files produced by some Windows programs (such as Excel's CSV export) that begin with a byte-order mark; plainutf-8would leave an invisible\ufeffat the start of the first line. - ▸
Large model datasets are usually
.jsonl(one JSON object per line, Topic 7.7), precisely because that format can be streamed line by line with the loop on this page. - ▸
To find every place a codebase relies on the locale default, run it with
python -X warn_default_encoding(or setPYTHONWARNDEFAULTENCODING=1): since Python 3.10 eachopen()withoutencodingthen emits anEncodingWarning.PYTHONUTF8=1(or-X utf8) already switches the default to UTF-8 today, andencoding="locale"(3.10) says explicitly that you want the system encoding. Note that data in other files, such as a CSV from a colleague's Windows machine, may still be cp1252, so the default changing does not remove the need to know the real encoding of the input. - ▸
Bad bytes do not have to be fatal or lossy:
errors="surrogateescape"turns undecodable bytes into lone surrogate characters and turns them back into the same bytes on writing, so a tool can pass through file names or text it cannot fully decode without damaging it. Also, once you have used a text file as an iterator (for line in f),f.tell()raisesOSError("telling position disabled by next() call"): usereadline()in awhileloop if you need positions.
Remember this
- 1
open(path, mode, encoding=...)returns a file object: a handle you use to read or write. The path is the file's name, such as"notes.txt", optionally with folders in front. A bare name means "in the folder where the program runs". The mode is a short string."r"reads (the default; the file must exist)."w"writes and erases whatever the file had before, creating it if needed."a"appends to the end."x"creates a new file and fails withFileExistsErrorif it exists, which protects you from overwriting. Adding"+"allows both reading and writing, and"b"means binary (raw bytes) instead of text. The default is text ("t"). - 2
Computers store only bytes: numbers from 0 to 255. An encoding is the agreement for turning characters into bytes and back. UTF-8 is the standard that can represent every character in every language and is used almost everywhere on the web. Plain English letters take one byte in UTF-8. Other characters take two to four (
étakes two,₹takes three). If the writer and the reader use different encodings, you get garbled text (mojibake) or aUnicodeDecodeError. - 3
Always write
encoding="utf-8"inopen, for both reading and writing. Without it Python uses a default that depends on the operating system and settings (often something else on Windows). The same program then behaves differently on two machines. Python 3.15 makes UTF-8 the default text encoding, which will remove this trap in time. Code that spells outencoding="utf-8"is correct on every version and every machine, so keep writing it. - 4
To read:
f.read()returns the whole file as one string,f.readline()returns one line,f.readlines()returns a list of lines. The best way to go through a file is to iterate over the file object:for line in f:(aforloop over the file, Topic 2.4, gives you one line per turn). It reads one line at a time from a buffer, so a 20 GB file uses almost no memory. Each line still ends with"\n". Useline.rstrip("\n")orline.strip()to remove it. This lazy reading is how big AI datasets in.jsonlfiles are processed. - 5
To write:
f.write(text)writes a string (not a number: convert withstr()or an f-string) and returns the number of characters written. It does not add a newline, so you write"\n"yourself.f.writelines(list_of_strings)writes many strings, also without newlines.print(text, file=f)writes with a newline added, which is often the neatest way. - 6
Newlines differ between systems: Windows ends lines with
\r\n, Linux and macOS with\n. In text mode Python hides this. When reading, every kind of line ending becomes"\n". When writing,"\n"becomes the system's ending. Leave this default alone for ordinary files. When you must keep line endings exactly (a CSV file, Topic 7.7), passnewline="". - 7
And always use
with(Topic 7.4) so the file is closed and the data is flushed (pushed out of the buffer onto the disk).
Explain it without notes
Explain the difference between the modes "r", "w", "a" and "x".
Why should you always pass encoding="utf-8" to open, even though Python 3.15 makes UTF-8 the default?
How do you process a file too big for memory? Why does iterating a file object work?
What happens to newline characters when you read and write text files, and what is newline="" for?
Practice
Write the numbers 1 to 5, one per line, to a file called numbers.txt. Then read the file back and print the sum of the numbers (it should print 15). Hint: each line is text, so convert it with int().
Write a function count_lines(path) that returns how many non-empty lines a text file has, reading it line by line (not with read()). Call it on numbers.txt. Hint: a line that is empty after strip() should not be counted.
Append the line done to numbers.txt (mode "a"), then print the file's last line. Hint: loop over the lines and remember the latest one.
Trade-offs
- ↔
**
read()versus iterating.**read()is convenient for small files and when you need the whole text (a regular expression over everything). Iterating scales to any size but gives you only one line at a time. - ↔
Strict versus forgiving decoding. The default
errors="strict"stops on damaged text so you notice.errors="replace"keeps going and inserts\ufffdcharacters, which can hide data problems. Choose strict unless losing a few characters is acceptable.
Done when you can
Done when you can choose between modes
r,w,aandxand say what each does to existing content.Done when you can read a file by iterating over lines and strip the newline.
Done when you can explain what an encoding is and why
encoding="utf-8"is always written.Done when you can explain what happens to
\non reading and writing, and when to usenewline="".Done when you can handle
FileNotFoundErrorandUnicodeDecodeErrorsensibly.