Python File Handling: One Word (with) Saves Your Work
š Full written solution: https://interview-kit-fe.vercel.app/python-lesson-15-working-with-files š» Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/python-lesson-15-working-with-files Chapters: 0:00 Close This Program And Everything You Made Is Gone 0:13 Key Terms, Plainly 0:29 Opening a File 0:44 The Problem With Manual Closing 0:59 Here's the Trick: with 1:15 Writing to a File 1:32 Reading Line by Line 1:47 What Is pathlib? 2:02 Using pathlib 2:18 Common Mistake: Wrong Path 2:37 Common Mistake: Overwrite Surprise 2:53 Manual vs with vs pathlib 3:11 Recap Learn how Python reads and writes text files safely, why forgetting to close a file can silently erase your work, and how the with statement fixes it for good. We cover writing files, reading line by line, and using pathlib for clean, cross-platform file paths. Includes two common mistakes: wrong paths and accidental overwrites. #python #pythontutorial #pathlib #filehandling #learnpython Watch next: - Always Pair With ORDER BY #shorts: https://youtu.be/pHZXlAKCR68 - Sorting with ORDER BY #shorts: https://youtu.be/OpETVzevDok - Python Lesson 13: Turn 4 Lines Into 1 With Comprehensions: https://youtu.be/-AV9Nakus-U
Introduction
Every program you write loses its memory the instant it stops running. Variables, lists, dictionaries ā all of it vanishes the moment the process exits. Files are the escape hatch. They're how a program's work outlives the program itself, and that makes file handling one of the most practical skills in everyday Python, not just an academic topic.
It's also a favorite subject in coding interviews and take-home assignments, because it exposes whether you actually understand resource management, not just syntax. Interviewers ask about it when reviewing log processing, data pipeline, or config-loading code, because file bugs are notoriously silent: nothing crashes, nothing throws a big red error, your data just quietly isn't there when you go looking for it.
This lesson is about the smallest word in Python that fixes the biggest category of file bugs: with. Along the way, we'll cover file modes, reading line by line, and why pathlib has replaced string-glued file paths in modern Python.
Problem Overview
Here's the situation, in plain terms: you want to open a file, do something with it (read its contents or write new content), and then make sure the file is properly closed when you're done.
That sounds trivial, but it hides four separate concepts that are easy to blur together:
- File ā a named container of data sitting on disk.
- Path ā the address that locates that file on the filesystem.
- Mode ā the instruction you give when opening it: read, write, or append.
- Handle ā the temporary connection object Python gives you so you can interact with the file's contents.
The core problem is this: opening a file grabs a system resource, and if you forget to release that resource (close the handle), you get bugs that don't announce themselves. Data doesn't get flushed to disk. Other parts of your program ā or other programs entirely ā can't access the file. And if your program crashes between opening and closing, the closing code never runs at all.
The goal isn't just "know how to call open()." It's understanding why the safe way to open a file looks different from the naive way, and why that difference matters.
Example
Say you're building a simple note-taking script. You want to write a line to notes.txt, then later read it back.
# Naive version
f = open("notes.txt", "w")
f.write("Buy milk\n")
f.close()
This works ā as long as nothing goes wrong. If f.write() raises an exception (say, the disk is full), execution jumps past f.close() entirely, and the file handle stays open until the operating system eventually cleans it up.
Now compare:
with open("notes.txt", "w") as f:
f.write("Buy milk\n")
Same result when everything goes right. But if f.write() raises an exception here, Python still closes the file before the exception propagates further. That's the entire value proposition of with in one example.
Intuition
Imagine you're a librarian. Someone checks out a book manually ā they write their name on a card, take the book, and are supposed to write "returned" on the card when they bring it back. Most of the time, that works. But people forget. People get hit by a bus (metaphorically ā a crash) on the way to return it. The card never gets updated, and now the system thinks that book is out forever, even though it's back on the shelf.
An experienced engineer doesn't try to fix this by reminding people harder to close things. They fix it by removing the human step entirely. That's what with does ā it's not a new feature bolted onto file handling, it's a structural guarantee: "whatever happens inside this block, I will clean up after you when it ends." This pattern ā acquire a resource, guarantee its release, regardless of how the block exits ā is called a context manager, and files are the most common example you'll meet it through.
Once you see file handling this way, the mode letters ("w", "a", "r") stop being arbitrary syntax and become intentional decisions: are you starting fresh, or adding to what's already there? That's a decision worth pausing on every single time you type open().
Brute Force Solution
The "brute force" approach here is the manual open/close pattern:
f = open("data.txt", "r")
contents = f.read()
f.close()
Idea: Explicitly manage the file handle's lifecycle yourself.
Algorithm:
- Call
open()with a path and mode. - Do whatever reading/writing you need.
- Call
.close()when finished.
Advantages:
- Explicit ā every step is visible, nothing implicit happening.
- Works fine for short, linear scripts with no branching logic.
Disadvantages:
- Any exception raised between open and close skips the close call.
- Easy to forget the close line entirely, especially as functions grow.
- Doesn't scale ā as soon as you add early returns or conditional branches, you need
try/finallyto guarantee cleanup, which is more code thanwithfor the same guarantee.
Time complexity: O(n) where n is the number of bytes/lines read or written ā the mode of opening doesn't change this.
Space complexity: O(n) if you read the whole file into memory at once (e.g., .read()), or O(1) additional space if you stream line by line.
Optimal Solution
The optimal solution is the with statement, paired with the right mode and pathlib for paths.
Step 1 ā Open safely with with:
with open("data.txt", "r") as f:
contents = f.read()
The moment the indented block ends ā normally or via exception ā f.close() runs automatically.
Step 2 ā Pick the correct mode:
| Mode | Meaning | Effect on existing content |
|---|---|---|
"r" |
Read | File must exist; unchanged |
"w" |
Write | Erases everything, starts blank |
"a" |
Append | Adds to the end, existing content untouched |
Step 3 ā Read large files line by line instead of all at once:
with open("data.txt", "r") as f:
for line in f:
print(line.strip())
Looping directly over the file handle reads one line at a time instead of loading the entire file into memory ā critical once files get large.
Step 4 ā Build paths with pathlib instead of string concatenation:
from pathlib import Path
folder = Path("data")
file_path = folder / "notes.txt"
The / operator here isn't division ā it's path joining, and it produces correct separators whether you're on Windows (\) or macOS/Linux (/).
Python Code
from pathlib import Path
def write_note(folder: str, filename: str, text: str) -> Path:
"""Append a line of text to a note file, creating the folder if needed."""
dir_path = Path(folder)
dir_path.mkdir(parents=True, exist_ok=True)
file_path = dir_path / filename
with open(file_path, "a") as f:
f.write(text + "\n")
return file_path
def read_notes(file_path: Path) -> list[str]:
"""Read all lines from a note file, stripped of trailing newlines."""
if not file_path.exists():
return []
with open(file_path, "r") as f:
return [line.strip() for line in f]
if __name__ == "__main__":
path = write_note("data", "notes.txt", "Buy milk")
print(read_notes(path))
Code Walkthrough
Path(folder)wraps the folder name in aPathobject so all later joins and checks are OS-independent.dir_path.mkdir(parents=True, exist_ok=True)creates the folder if it doesn't exist yet, without erroring if it already does ā this sidesteps a common "file not found" bug caused by a missing parent directory.dir_path / filenamejoins the folder and filename into a full path usingpathlib's overloaded/operator.with open(file_path, "a") as f:opens in append mode, so callingwrite_noterepeatedly adds new lines instead of wiping old ones, and guarantees the file closes even iff.write()fails.read_noteschecksfile_path.exists()first, avoiding aFileNotFoundErrorfor a file that was never written.[line.strip() for line in f]iterates the handle line by line ā memory-efficient ā and strips the newline character each line carries.
Dry Run
Call write_note("data", "notes.txt", "Buy milk"):
dir_pathbecomesPath("data").mkdircreates thedata/folder (or does nothing if it exists).file_pathbecomesPath("data/notes.txt").with open(file_path, "a") as f:opens (creating the file if absent), writes"Buy milk\n", then closes automatically.- Function returns
Path("data/notes.txt").
Call read_notes(Path("data/notes.txt")):
file_path.exists()returnsTrue.with open(...) as f:opens for reading.- The list comprehension iterates the file, producing
["Buy milk"]. - File closes automatically as the
withblock ends.
Output: ['Buy milk']
Complexity Analysis
- Time: O(n) for both writing and reading, where n is the size of the text being written or the number of lines being read. Opening and closing a file handle is O(1) overhead regardless of file size.
- Space: O(1) additional space for
write_note(streaming write).read_notesuses O(n) space because it materializes every line into a list ā reasonable for note files, but worth reconsidering for very large files where a generator would be better. - These bounds are correct because neither function loads more into memory than the data it's actually asked to produce or consume; there's no hidden buffering step.
Alternative Solutions
For a quick one-shot read, pathlib offers a shortcut that skips open and with entirely:
contents = Path("data.txt").read_text()
This is convenient and perfectly safe for small files ā it internally handles opening and closing for you. The tradeoff: it loads the entire file into memory at once, so it's a poor choice for anything beyond a few megabytes. For large files, the line-by-line loop from the optimal solution remains the right tool.
Edge Cases
- Empty file: Reading returns an empty string (
.read()) or zero iterations (looping) ā no error, just nothing to process. - File doesn't exist, mode
"r": RaisesFileNotFoundError. Always check.exists()first, or catch the exception, if the file's presence isn't guaranteed. - File doesn't exist, mode
"w"or"a": Both modes create the file if it's missing ā no error. - Wrong mode on an existing important file (
"w"instead of"a"): Silently erases all prior content with no confirmation prompt. This is the single most damaging mistake in this lesson. - Relative path run from the wrong working directory: Produces
FileNotFoundErroreven though the file genuinely exists ā just not relative to where the process happened to start. - Very large files read with
.read_text()or.read(): Can exhaust memory; prefer line-by-line iteration.
Common Mistakes
- Using mode
"w"when you meant"a", wiping out existing data with no warning. - Forgetting to close a manually-opened file, leaking the handle if an exception occurs.
- Hardcoding relative paths that only work when the script is run from one specific directory.
- Gluing paths together with string concatenation and hardcoded
/or\, breaking on the other operating system. - Loading an entire large file into memory with
.read()when line-by-line iteration would suffice. - Forgetting to strip the trailing newline character when comparing or processing lines read from a file.
- Assuming
open()in read mode will create the file if it's missing ā it won't, and raisesFileNotFoundErrorinstead.
Interview Questions
- What does the
withstatement actually do under the hood? (Hint:__enter__and__exit__.) - What's the difference between
"w","a","x", and"r+"modes? - How would you read a 10 GB file without running out of memory?
- Why does
pathlibhandle cross-platform paths better than string concatenation? - How do you safely write to a file such that a crash mid-write can't corrupt existing data? (Hint: write to a temp file, then atomically rename.)
- What happens if you open a file in
"r"mode and it doesn't exist?
Similar Problems
- LeetCode-style log/text processing problems (e.g., parsing structured text into records) ā they all begin with correctly opening and iterating a file.
- Design a rate limiter using a file-based counter ā tests understanding of read-modify-write cycles and file locking.
- Design an in-memory file system (LeetCode 588) ā reinforces the file/path/handle mental model in a data-structure context.
- CSV/JSON parsing exercises ā build directly on top of safe file-opening patterns covered here.
Key Takeaways
- Files let data survive after your program exits ā that's the entire point of learning this.
withguarantees a file closes even when something crashes mid-operation; manualopen/closedoesn't.- Mode letters are a decision, not decoration:
"w"erases,"a"appends,"r"requires the file to exist. - Reading line by line scales to large files; loading everything with
.read()doesn't. pathlibremoves an entire class of cross-platform path bugs that string concatenation used to cause.
Watch the Video
For the full walkthrough with live coding, watch the video here: {LINK}
About the Series
This lesson is part of Fun with Learning Technology, a series that breaks down programming and computer science fundamentals into short, practical lessons ā built for developers who want to actually understand why something works, not just memorize syntax.
Call To Action
If this cleared something up for you, drop a like on the video and subscribe on YouTube so future lessons reach you. For deeper write-ups like this one delivered straight to your inbox, subscribe to the Substack. And if you've got a file-handling war story of your own, share it in the comments ā those twenty-minute "wrong directory" bugs are basically a rite of passage.
The solution
from pathlib import Path
folder = Path("data")
file_path = folder / "notes.txt"
if file_path.exists():
print(file_path.read_text())Ready to try it yourself? Solve Python problems with instant feedback in the practice sandbox.
Practice now ā


