Python File Handling โ open(), read(), and write() Explained Properly
Everything you actually need to know about working with files in Python โ open(), file modes, the with statement and why it matters, reading and writing text, CSV, and JSON, and the exact FileNotFoundError you'll hit while learning this.
Last Updated
March 2026
Read Time
20 min
Level
Beginner
What is File Handling in Python?
File handling is how a Python program reads data from files on disk, writes data back out to them, and manages everything in between โ creating new files, appending to existing ones, and, critically, closing them properly when you're done. Almost every real program eventually needs this: reading a configuration file, saving user data, processing a CSV export, logging errors to a text file. Python's built-in open() function is the single entry point for essentially all of it.
The basic shape looks almost deceptively simple: f = open("data.txt", "r") opens a file for reading, f.read() pulls its entire contents into a string, and f.close() releases the file. Three lines, done. But that simplicity hides a genuine trap: if anything goes wrong between opening and closing โ an exception gets raised, a return statement exits the function early โ f.close() never runs, and the file stays open longer than it should, potentially locking other programs out of it or leaking a file descriptor your operating system eventually runs out of if it happens enough times.
This is exactly the problem the with statement was built to solve, and it's the single most important habit this entire topic teaches: with open("data.txt", "r") as f: guarantees the file gets closed automatically the moment that block ends, whether it ends normally or because of an error. I genuinely don't write open() without with anymore, for anything, and once you understand exactly why, you probably won't either โ it's not a stylistic preference, it's a correctness guarantee that manual close() calls simply can't match.
This guide covers open() and its file modes precisely, why with genuinely matters (not just as a best practice, but mechanically), the different ways to read and write content, working with CSV and JSON files using the standard library, text encoding, and the specific FileNotFoundError traceback that every Python developer eventually hits.
History โ How Python's File Handling Evolved
File I/O has been core to Python since its earliest releases, but the specific mechanisms for doing it safely and correctly have genuinely improved over the language's history.
- โถ
1991 โ Python 0.9.0 โ The open() built-in function was present from Python's earliest public release, providing basic file reading and writing capability modelled conceptually on C's file I/O functions.
- โถ
Early Python โ Manual close() Was the Only Option โ Before context managers existed, every file had to be closed manually with f.close(), and a genuine, common bug pattern was forgetting to close a file, or failing to close it when an exception occurred partway through processing.
- โถ
2006 โ Python 2.5 (PEP 343) โ Introduced the with statement and the broader context manager protocol (__enter__ and __exit__ methods), giving Python a general-purpose mechanism for guaranteed setup/cleanup โ file handling became its single most common and immediately obvious use case.
- โถ
2008 โ Python 3.0 โ Made a fundamental, sometimes-confusing-at-first change: files opened in text mode now return str objects (proper Unicode text) by default, while binary mode ('rb', 'wb') returns bytes objects โ a genuine, deliberate split that Python 2 handled far less strictly.
- โถ
2012 โ Python 3.3 (PEP 3151) โ Reorganised Python's built-in exception hierarchy for OS-related errors, consolidating several separate exceptions into a cleaner structure โ FileNotFoundError became a distinct, specific subclass of the broader OSError, rather than a generic IOError with a matching error number.
- โถ
2019 โ Python 3.6+ (pathlib maturity) โ The pathlib module, introduced in Python 3.4, matured into the recommended, object-oriented way to handle file paths, offering a cleaner alternative to string-based path manipulation via the older os.path module, while open() itself remained the standard tool for actual file content I/O.
- โถ
2026 โ Current state โ open() combined with the with statement remains the standard, expected way to handle files in Python. pathlib.Path objects are increasingly common for path manipulation, and both integrate cleanly โ Path objects even have their own .open() method that behaves identically to the built-in function.
Key Characteristics of Python File Handling
Here are the 11 things worth knowing precisely about how file handling actually works in Python:
Every file operation starts with open(filepath, mode), returning a file object that supports reading, writing, or both, depending on the mode string passed.
'r' for read, 'w' for write (overwrites existing content), 'a' for append, 'x' for exclusive creation (fails if the file exists), and 'b' appended to any of these switches to binary mode instead of text mode.
The with statement ensures a file's close() method runs automatically when the block exits, even if an exception occurs partway through โ this is a genuine correctness guarantee, not just tidier syntax.
.read() pulls the entire file as one string, .readline() reads a single line, .readlines() returns all lines as a list, and iterating directly over the file object (for line in f:) reads it lazily, one line at a time.
Unlike print(), f.write() does NOT append a newline character automatically โ writing multiple lines requires explicitly including \n in each call, or writing pre-joined multi-line text.
Reading or writing advances an internal position marker within the file. f.tell() reports the current position, and f.seek(offset) moves it, letting you re-read from the beginning or jump to a specific byte.
Text mode ('r', 'w') automatically decodes bytes to str using a specified or platform-default encoding. Binary mode ('rb', 'wb') returns raw bytes objects with no decoding at all โ required for images, executables, and other non-text files.
open(path, 'r', encoding='utf-8') explicitly sets the text encoding. Omitting it relies on the platform's default encoding, which can genuinely differ between operating systems and cause subtle, hard-to-reproduce bugs.
Attempting to open a nonexistent file in read mode ('r') raises FileNotFoundError immediately, with the exact filename included in the error message.
The csv module handles comma-separated files correctly (including edge cases like commas inside quoted fields), and the json module reads/writes JSON directly to and from Python dictionaries and lists.
Path objects from the pathlib module represent file paths as objects with methods (.exists(), .read_text(), .parent) rather than plain strings manipulated with os.path functions.
How File Handling Actually Executes โ Flowchart
Opening, using, and closing a file follows a specific, precise sequence โ and the with statement's real value only becomes clear once you see exactly where it inserts guaranteed cleanup, even on the failure path.
Code Execution Flow โ from source to output
Key insight: step 7 โ f.close() being called automatically โ happens on both paths out of the block, the successful one and the failing one. This is precisely what manual open()/close() without with cannot guarantee: if an exception fires between those two calls, a bare f.close() written after your logic simply never executes, leaving the file open.
How File Handling Actually Works โ Modes, with, and the Errors They Explain
Three things separate someone who's copied a basic open()/read() snippet from someone who genuinely understands file handling: knowing exactly what each mode string does, understanding precisely why with is a correctness tool and not just a style choice, and recognising the specific errors that come from mismatched modes or missing files.
๐ค File Modes โ What Each Letter Actually Does
'r' (read, the default if omitted) opens an existing file for reading and fails with FileNotFoundError if it doesn't exist. 'w' (write) creates a new file, or โ importantly โ completely overwrites an existing file's content with nothing, the instant it's opened, before you've even written anything. 'a' (append) opens the file for writing, but positions the cursor at the END, adding new content after whatever's already there rather than erasing it. 'x' (exclusive creation) creates a new file but raises FileExistsError if a file with that name already exists โ useful specifically when you want to guarantee you're not silently overwriting something. Appending 'b' to any of these ('rb', 'wb') switches from text mode to binary mode, returning bytes objects instead of decoded str text โ required for images, PDFs, or any non-text file.
โ ๏ธ The Mode Mistake That Silently Destroys Data
This is the single most consequential file-handling mistake, and it doesn't raise an error at all โ it just quietly, silently erases data. Opening a file with 'w' mode when you actually meant 'a' (or meant to read it first) instantly truncates the existing file to zero bytes, the moment open() succeeds, regardless of whether you go on to write anything at all. There's no confirmation prompt, no warning โ the previous content is simply gone. I've genuinely lost data this way once, early on, opening a log file with 'w' inside a loop that ran on every iteration, wiping the file back to empty every single time instead of appending. The fix was one letter.
๐ก๏ธ Why with Is a Correctness Guarantee, Not Just Cleaner Syntax
Writing f = open(path); ...code...; f.close() works fine exactly as long as nothing between the open and the close ever raises an exception. The moment something does โ a KeyError from bad data, a network timeout mid-processing, anything โ Python jumps straight to propagating that exception, skipping every line after the point of failure, including your f.close() call. The file stays open. with open(path) as f: fundamentally changes this: it's built on Python's context manager protocol, which guarantees the file's __exit__ method (which calls close() internally) runs no matter how the block is exited โ normal completion, a return statement, or an unhandled exception. This is why every serious Python style guide, and essentially every code review, treats bare open() without with as something to flag.
๐ฅ FileNotFoundError โ The Exact Trigger
Opening a file that doesn't exist in read mode raises FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'. This is almost always caused by one of two things: a genuine typo in the filename or path, or โ very commonly โ a path that's correct relative to where you expected the script to run, but wrong relative to where it actually ran from, since Python resolves relative paths against the current working directory at the moment the script executes, not the directory the script file itself lives in.
Simple rule to remember: always use with open(...), never bare open() without it. Double-check 'w' versus 'a' before running anything that touches a file you care about. Specify encoding='utf-8' explicitly for text files rather than relying on the platform default.
File Modes โ Key Differences
Each mode string produces genuinely different behaviour, and choosing the wrong one is exactly how data gets silently lost or a script crashes unexpectedly.
File Handling โ Python vs Other Languages
Python's approach to guaranteed resource cleanup, through with and context managers, is genuinely cleaner than the equivalent pattern in several widely-used languages.
Here's my honestly opinionated take: Python's with statement, applied to file handling, is one of the cleanest solutions to the 'guaranteed cleanup' problem I've used across any language. Java's try-with-resources achieves the same correctness guarantee but needs more visible ceremony around it. C simply doesn't have a language-level answer at all โ fclose() forgotten inside an early-return branch is a genuinely common, real bug in C code, precisely because nothing enforces it. Python's version reads almost like plain English โ with open(...) as f: โ while quietly doing something structurally important underneath.
Advantages and Disadvantages of Python's File Handling
Python's file handling model is genuinely well-designed, but it still has sharp edges worth knowing about explicitly.
File Handling Architecture โ From open() to the OS
The diagram below shows the full stack a file operation passes through โ from your Python code down to the actual operating system file descriptor.
File Handling in Practice โ Text, CSV, JSON, and the Classic Error
Here's file handling covering the essentials โ writing, reading line by line, CSV, and JSON โ followed by the exact FileNotFoundError and the fix.
# Writing text to a file โ 'w' creates/overwrites
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("First line\n")
f.write("Second line\n")
# Appending more content without erasing existing data
with open("notes.txt", "a", encoding="utf-8") as f:
f.write("Third line, appended\n")
# Reading line by line โ memory-efficient for large files
with open("notes.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())Output
First line Second line Third line, appendedNow working with CSV and JSON, using the standard library's dedicated modules:
import csv
import json
# Writing CSV correctly, including a field with a comma in it
rows = [
{"name": "Priya", "city": "Pune, MH"},
{"name": "Rohan", "city": "Delhi"},
]
with open("users.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "city"])
writer.writeheader()
writer.writerows(rows)
# Reading it back correctly, comma-in-field handled automatically
with open("users.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row)
# JSON โ writing a Python dict directly to a file
data = {"name": "Priya", "age": 25, "active": True}
with open("user.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# Reading JSON back as a Python dict
with open("user.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["name"], loaded["age"])Output
{'name': 'Priya', 'city': 'Pune, MH'} {'name': 'Rohan', 'city': 'Delhi'} Priya 25And the classic error โ reproduced exactly:
with open("does_not_exist.txt", "r") as f:
content = f.read()Output
Traceback (most recent call last): File "missing_file.py", line 1, in <module> with open("does_not_exist.txt", "r") as f: FileNotFoundError: [Errno 2] No such file or directory: 'does_not_exist.txt'Practice This Code โ Live Editor
Line-by-Line Explanation
- โถ
with open("notes.txt", "w", ...) as f:โ'w'creates the file if it doesn't exist, or truncates it to empty first if it does โ every run of this specific block starts from a clean slate. - โถ
with open("notes.txt", "a", ...) as f:โ'a'opens with the cursor at the END of existing content, so this new write is added AFTER the two lines from the previous block, not replacing them. - โถ
for line in f:โ Iterates over the file object directly, reading one line at a time. This is memory-efficient because it never loads the whole file into memory at once, unlikef.read(). - โถ
csv.DictWriterandnewline=""โ Thecsvmodule correctly handles a comma embedded inside a field value like"Pune, MH"by quoting it in the output, something manual string joining with commas would get wrong.newline=""prevents the csv module from writing extra blank lines on Windows. - โถ
json.dump(data, f, indent=2)โ Converts the Python dictionary directly into JSON text and writes it to the file in one call, withindent=2producing nicely formatted, human-readable output rather than one dense line.
Where File Handling Actually Matters โ Real-World Applications
File handling isn't a standalone topic that stays in a textbook โ it's woven into nearly every category of real Python program:
- โถ
โ๏ธ Configuration Files โ Applications routinely read settings from a config file at startup (JSON, YAML, or .ini format), letting behaviour be changed without editing the actual source code โ a genuinely standard pattern across scripts and full applications alike.
- โถ
๐ Data Analysis and CSV Processing โ Reading a CSV export from a spreadsheet or database, processing it, and writing results back out to a new file is one of the most common practical file-handling tasks in data-focused Python work, often paired with the pandas library for larger datasets.
- โถ
๐ Logging Application Behaviour โ Writing timestamped log entries to a file, typically in append mode, is standard practice for tracking what a long-running script or service actually did, especially useful for diagnosing issues after the fact.
- โถ
๐ API Response Caching โ Saving the result of an expensive API call to a local JSON file avoids re-fetching the same data repeatedly, a common pattern in scripts that would otherwise hit rate limits or slow external services unnecessarily.
- โถ
๐ผ๏ธ Working with Binary Files โ Downloading and saving images, PDFs, or other binary content requires opening files in binary mode ('wb'/'rb'), correctly handling raw bytes rather than decoded text.
- โถ
๐งช Test Fixtures and Sample Data โ Automated test suites frequently read sample input files and compare processing results against expected output files, making reliable, predictable file handling a genuine prerequisite for trustworthy testing.
- โถ
โ๏ธ Backup and Data Export Scripts โ Scripts that periodically export data from a database or application into files (for backup, sharing, or archival) rely directly on correct write and append handling to avoid data loss.
- โถ
๐ Batch Processing Multiple Files โ Combined with the os or pathlib modules for listing directory contents, file handling scripts commonly loop through many files in a folder, applying the same read/process/write logic to each one automatically.
Why File Handling Deserves Genuine, Careful Attention
File handling looks simple enough that it's tempting to treat it as a minor topic, but the consequences of getting it wrong are genuinely higher than most beginner topics. Here's why it's worth real attention:
- โถ
๐พ Mistakes Here Can Genuinely Destroy Data โ Unlike most beginner bugs that just produce a wrong number or an error message, a misused 'w' mode can permanently erase real content with zero warning โ the stakes for getting this right are simply higher than most early topics.
- โถ
๐ก๏ธ It's the Clearest Real-World Case for Context Managers โ Understanding exactly why with matters for files builds a genuinely transferable mental model for Python's broader context manager protocol, which shows up again with database connections, network sockets, and locks.
- โถ
๐ผ It's a Practical Skill Tested in Real-World Coding Assessments โ Take-home coding tests and practical interview exercises frequently involve reading a data file and processing it โ genuine file-handling fluency, not just theoretical knowledge, gets tested directly.
- โถ
๐ Encoding Bugs Are a Genuine, Recurring Real-World Problem โ Text encoding issues that only surface when code runs on a different machine or operating system are a real, documented category of production bugs โ understanding encoding= explicitly is a practical, not academic, skill.
- โถ
๐ It's Foundational for Data Science and Automation Work โ Nearly every data-focused or automation-focused Python task starts with reading input from somewhere and ends with writing output somewhere โ file handling is genuinely the connective tissue of that entire category of work.
- โถ
๐ฎ๐ณ It's a Standard, Practical Topic in Indian Data Analyst and Backend Interviews โ Practical exercises involving reading a CSV, processing it, and writing results are common in Indian data analyst and junior backend developer interview and assignment rounds, precisely because they test genuine, applied competence.
pathlib and Modern File Handling Practices
Beyond the basic open() function, a few modern patterns and standard library tools are worth knowing for writing genuinely robust file-handling code.
- โถ
pathlib.Path for path manipulation โ from pathlib import Path; p = Path('data') / 'users.csv' builds cross-platform-correct paths using the / operator, avoiding manual string concatenation and platform-specific separator issues entirely.
- โถ
Path.exists() and Path.is_file() โ Checking whether a file exists before attempting to open it, rather than relying purely on catching FileNotFoundError, is sometimes clearer for control flow, though catching the exception remains the more Pythonic approach for many cases (easier to ask forgiveness than permission).
- โถ
Path.read_text() and Path.write_text() โ Convenience methods that handle opening, reading/writing, and closing a text file in a single call โ Path('notes.txt').read_text(encoding='utf-8') is a compact alternative to the full with open(...) as f: pattern for simple cases.
- โถ
os.makedirs() / Path.mkdir(parents=True, exist_ok=True) โ Creating necessary parent directories before writing a file into a path that might not fully exist yet โ a common real-world need when generating output files into a fresh directory structure.
- โถ
shutil for higher-level file operations โ Copying, moving, and deleting entire files or directory trees are handled by the shutil module rather than open(), since these are whole-file operations rather than content-reading/writing operations.
- โถ
tempfile for genuinely temporary files โ When a script needs a scratch file that shouldn't persist, the tempfile module creates properly isolated, automatically-cleaned-up temporary files and directories rather than manually managing a file in a fixed, potentially-colliding location.
Python File Handling โ Interview Questions
These come up regularly in Python interviews, especially for roles involving data processing, automation, or backend work.
Practice Questions โ Test Your Understanding
Work through these before checking the answers. Predicting exactly what ends up in a file โ not just whether code runs without error โ is the real test here.
1. What happens to the existing content of data.txt (which currently contains 'old content') after running: with open('data.txt', 'w') as f: pass โ with nothing written inside the block at all?
Medium2. What is the output of a file after running: with open('log.txt', 'w') as f: f.write('Line 1') \n f.write('Line 2') โ assuming the file is then read back?
Easy3. What error occurs, and why, if you run: with open('report.txt', 'r') as f: content = f.read() โ when report.txt has never been created?
Easy4. Why would with open('big_file.txt', 'r') as f: lines = f.readlines() potentially be a poor choice for a genuinely very large file, and what's the better alternative?
Medium5. What does f.tell() return immediately after opening a file in 'r' mode and calling f.read() once on a 100-character file?
Medium6. What's the bug in this code, and what does it actually do? for filename in ['a.txt', 'b.txt', 'c.txt']: with open('combined.txt', 'w') as f: f.write(open(filename).read())
Hard7. What is the output of: import json \n data = {'active': True, 'count': 3} \n with open('x.json', 'w') as f: json.dump(data, f) \n with open('x.json', 'r') as f: result = json.load(f) \n print(type(result['active']))
Medium8. Why might code that works correctly when run directly (python script.py) raise FileNotFoundError when the exact same script is triggered from a different directory or by a scheduling tool like cron?
HardConclusion โ Small Mistakes Here Have Real Consequences
File handling is deceptively simple-looking Python โ three or four lines cover the common case โ but it's one of the few beginner topics where a genuinely small mistake, a single wrong letter in a mode string, can cause real, permanent, unrecoverable damage to actual data. That's worth taking seriously, not as a scare tactic, but as an honest description of why this topic deserves more careful attention than its simple syntax might suggest.
If you're a complete beginner, the priority is building the with open(...) as f: pattern into pure muscle memory, to the point where writing bare open() without it feels visibly wrong. If you're doing real, production-adjacent work, the priority shifts to precision: explicit encoding, careful mode selection, and knowing exactly which standard library tool โ csv, json, pathlib, plain open() โ fits the specific file format and task in front of you.
The habit worth building here: before running any script that opens a file in 'w' mode, pause for one extra second and confirm โ genuinely, deliberately โ that overwriting that specific file's content is exactly what you intend. That single habit prevents the one file-handling mistake in this entire topic that can't be undone by re-running the script.
File handling rewards care precisely because the cost of carelessness is real. Use with every time, choose your mode deliberately, and be explicit about encoding โ everything else about reading and writing files in Python follows naturally from those three habits. ๐