File Handling
Topic 6 of 7, with 3 concept checks. Reading, writing, and working with files safely
Treat files as resources with explicit lifetimes
Resource ownership
Choose text or binary mode deliberately, keep encoding and cursor position visible, and guarantee closure even when reading, parsing, or writing raises an exception.
Core lesson 01
with open(...) as f: guarantees f.close() runs even if the code inside raises, unlike manual open()/close() where an exception before close() leaks the file handle.
A file object is a context manager — __enter__ returns the file itself, __exit__ closes it. Using with means you never have to remember to call .close(), and critically, the file still gets closed if something goes wrong while reading or writing, which a plain f = open(...); ...; f.close() sequence does not guarantee.
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("line 1\n")
f.write("line 2\n")
# file is guaranteed closed here, even if write() had raised
with open("notes.txt", encoding="utf-8") as f:
content = f.read()
print(content)What to remember
Why should you use with open(...) as f: instead of manually calling open()/close()?
Common footguns
- Forgetting that opening in 'w' mode truncates (erases) an existing file immediately — use 'a' to append instead.
Core lesson 02
Text mode ('r'/'w', the default) reads/writes str and handles encoding plus newline translation automatically. Binary mode ('rb'/'wb') reads/writes raw bytes with no decoding or translation.
Text mode decodes bytes from disk into str using an encoding (best made explicit with encoding="utf-8") and translates line endings. Binary mode gives you the raw bytes exactly as stored, with none of that — necessary for non-text files (images, archives) or when you need exact byte-for-byte control.
with open("data.bin", "wb") as f:
f.write(b"\x00\x01\x02")
with open("data.bin", "rb") as f:
raw = f.read()
print(raw) # b'\x00\x01\x02'
print(type(raw)) # <class 'bytes'>
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("cafe au lait") # str, encoded to utf-8 bytes on writeWhat to remember
What's the difference between text mode and binary mode when opening files?
Common footguns
- Opening a binary file (image, zip) in text mode — decoding likely raises UnicodeDecodeError or silently corrupts the data.
- Omitting encoding= on open() and getting different results on different machines — always pass encoding="utf-8" explicitly for text files.
Core lesson 03
Iterate the file object directly with a for loop — it yields one line at a time lazily, so memory use stays constant regardless of file size, unlike f.read() or f.readlines().
f.read() loads the entire file into a single string; f.readlines() loads it into a list of lines — both use memory proportional to file size. A file object is itself an iterator over its lines, so for line in f: reads and yields one line at a time, discarding it once processed, using O(1) memory regardless of whether the file is a few lines or a few gigabytes.
with open("huge_log.txt", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
print(line.strip())
# never holds the whole file in memory at onceWhat to remember
How do you read a large file line-by-line without loading it all into memory?
Common footguns
- Calling f.readlines() 'just to be safe' on a file that might be huge — defeats the whole point and can exhaust memory.
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Always specify encoding="utf-8" explicitly when opening text files.
- Always use with open(...) rather than manual open/close.
- Iterate large files line-by-line instead of reading them fully into memory.
- Use pathlib.Path instead of raw string paths for new code.
Apply the concept in Interview practice
Read N Characters Given Read4easyLeetCode #157 · O(n) time
Repeatedly call the given read4(buf) API to fill a 4-character internal buffer, copying out only as many characters as still needed into the destination buffer.
Open problemRead N Characters Given Read4 II - Call Multiple TimeshardLeetCode #158 · O(n) time across all calls
Keep leftover characters from a previous read4() call in a persistent buffer between invocations, since a prior call may have read more than was consumed.
Open problemConcept checks
Why should you use with open(...) as f: instead of manually calling open()/close()?
Hint
It's the same context-manager guarantee you saw earlier, applied to files.
The file gets closed even if an exception happens while reading/writing.
Answer
with open(...) as f: guarantees f.close() runs even if the code inside raises, unlike manual open()/close() where an exception before close() leaks the file handle.
A file object is a context manager — __enter__ returns the file itself, __exit__ closes it. Using with means you never have to remember to call .close(), and critically, the file still gets closed if something goes wrong while reading or writing, which a plain f = open(...); ...; f.close() sequence does not guarantee.
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("line 1\n")
f.write("line 2\n")
# file is guaranteed closed here, even if write() had raised
with open("notes.txt", encoding="utf-8") as f:
content = f.read()
print(content)Watch out
- Forgetting that opening in 'w' mode truncates (erases) an existing file immediately — use 'a' to append instead.
What's the difference between text mode and binary mode when opening files?
Hint
One works with str, the other with bytes.
Text mode also handles encoding and newline translation for you.
Answer
Text mode ('r'/'w', the default) reads/writes str and handles encoding plus newline translation automatically. Binary mode ('rb'/'wb') reads/writes raw bytes with no decoding or translation.
Text mode decodes bytes from disk into str using an encoding (best made explicit with encoding="utf-8") and translates line endings. Binary mode gives you the raw bytes exactly as stored, with none of that — necessary for non-text files (images, archives) or when you need exact byte-for-byte control.
with open("data.bin", "wb") as f:
f.write(b"\x00\x01\x02")
with open("data.bin", "rb") as f:
raw = f.read()
print(raw) # b'\x00\x01\x02'
print(type(raw)) # <class 'bytes'>
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("cafe au lait") # str, encoded to utf-8 bytes on writeWatch out
- Opening a binary file (image, zip) in text mode — decoding likely raises UnicodeDecodeError or silently corrupts the data.
- Omitting encoding= on open() and getting different results on different machines — always pass encoding="utf-8" explicitly for text files.
How do you read a large file line-by-line without loading it all into memory?
Hint
A file object is already an iterator.
Looping over it directly gives you one line at a time.
Answer
Iterate the file object directly with a for loop — it yields one line at a time lazily, so memory use stays constant regardless of file size, unlike f.read() or f.readlines().
f.read() loads the entire file into a single string; f.readlines() loads it into a list of lines — both use memory proportional to file size. A file object is itself an iterator over its lines, so for line in f: reads and yields one line at a time, discarding it once processed, using O(1) memory regardless of whether the file is a few lines or a few gigabytes.
with open("huge_log.txt", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
print(line.strip())
# never holds the whole file in memory at onceWatch out
- Calling f.readlines() 'just to be safe' on a file that might be huge — defeats the whole point and can exhaust memory.