Python File Read Write
Almost every real program needs to save data somewhere or load data that already exists — configuration files, logs, CSV exports, saved user settings. Python makes this easy with the built-in open() function, which gives you a file object you can read from or write to. This lesson covers everything you need to confidently read and write files: how open() works internally, every file mode, the methods you’ll use daily, and the mistakes that trip up almost every beginner.
Overview / How It Works
When you call open("data.txt", "r"), Python asks the operating system to locate the file and hands back a file object (technically a TextIOWrapper for text mode, or a BufferedReader/BufferedWriter for binary mode). This object keeps track of a position inside the file — an internal cursor that moves forward as you read or write. Nothing is loaded into memory all at once by default; Python reads from disk in buffered chunks for efficiency, which is why iterating over a file line by line is memory-friendly even for huge files.
A file must be opened before use and closed afterward. Closing releases the operating system’s file handle and, crucially, flushes any buffered writes to disk. If you never close a file you opened for writing, some or all of your data might never actually reach the disk. Python’s with statement (a context manager) closes the file automatically for you, even if an exception occurs — this is why it’s the standard, recommended way to work with files.
Files can be opened in text mode (the default), where Python automatically decodes bytes into str using a text encoding (UTF-8 by default on most modern systems), or binary mode, where you read and write raw bytes objects with no decoding at all — used for images, zip files, or any non-text data.
Syntax
file_object = open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None)
| Parameter | Meaning |
|---|---|
file |
Path to the file, as a string or Path object. |
mode |
How to open the file — see the mode table below. |
buffering |
Buffering policy; -1 uses the system default (usually best left alone). |
encoding |
Text encoding to use, e.g. "utf-8". Ignored in binary mode. Should always be set explicitly in text mode. |
errors |
How to handle encoding/decoding errors, e.g. "strict" or "replace". |
newline |
Controls how line endings are translated; usually left as default. |
File Modes
| Mode | Meaning |
|---|---|
"r" |
Read (default). Error if the file doesn’t exist. |
"w" |
Write. Creates the file if missing, truncates (erases) it if it exists. |
"a" |
Append. Creates the file if missing; writes go to the end of the file. |
"x" |
Exclusive creation. Fails with FileExistsError if the file already exists. |
"r+" |
Read and write; file must already exist. |
"b" suffix |
Binary mode, e.g. "rb" or "wb" — works with bytes instead of str. |
"t" suffix |
Text mode (the default) — can be written explicitly as "rt". |
Examples
Example 1: Writing and Then Reading a File
with open("greeting.txt", "w", encoding="utf-8") as file:
file.write("Hello, learners!\n")
file.write("Welcome to Python file handling.\n")
with open("greeting.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
Output:
Hello, learners!
Welcome to Python file handling.
The first with block opens greeting.txt in write mode, writes two lines (note the explicit \n newline characters — write() does not add them for you), and automatically closes the file at the end of the block. The second block reopens the file for reading and uses read() to pull the entire contents into a single string. Because content already ends in a newline, print(content) adds one more, leaving a blank line at the end of the output.
Example 2: Reading Line by Line
with open("greeting.txt", "r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
print(f"{line_number}: {line.strip()}")
Output:
1: Hello, learners!
2: Welcome to Python file handling.
A file object is iterable — looping over it directly yields one line at a time (including the trailing \n), which is the most memory-efficient way to process large files since only one line is held in memory at a time. .strip() removes the trailing newline (and any surrounding whitespace) before printing. enumerate(file, start=1) gives each line a 1-based line number.
Example 3: Appending Data and Counting Lines
lines_to_add = [
"Files are opened using open().\n",
"Always close them or use a with block.\n",
]
with open("greeting.txt", "a", encoding="utf-8") as file:
file.writelines(lines_to_add)
with open("greeting.txt", "r", encoding="utf-8") as file:
all_lines = file.readlines()
print(all_lines)
print(f"Total lines: {len(all_lines)}")
Output:
['Hello, learners!\n', 'Welcome to Python file handling.\n', 'Files are opened using open().\n', 'Always close them or use a with block.\n']
Total lines: 4
"a" mode opens the file without erasing existing content and positions the cursor at the end, so writelines() — which writes a list of strings without adding newlines itself — appends the two new lines after the original two. readlines() then returns every line as a list of strings, each still containing its trailing \n, which is why the printed list shows four elements.
Under the Hood
Understanding what happens when you call these functions helps you avoid subtle bugs:
open()asks the OS for a file descriptor (a small integer handle the kernel uses internally) and wraps it in a Python object that tracks the current read/write position, the chosen encoding, and an internal buffer.- Buffering means writes aren’t necessarily sent to disk immediately — they sit in memory until the buffer fills, you call
file.flush(), or the file is closed. This is why data can appear “missing” if a program crashes before closing the file properly. with open(...) as file:callsfile.__enter__()at the start of the block and guaranteesfile.__exit__()— which callsclose()— runs when the block ends, whether it ends normally or via an exception. This is equivalent to atry/finallythat closes the file, but shorter and harder to forget.- The cursor moves forward as you read or write. You can inspect it with
file.tell()and move it withfile.seek(offset), which is how programs jump to a specific byte position without reading everything before it. - Text vs. binary: in text mode, every
read()/write()call encodes or decodes bytes using the specifiedencoding. In binary mode ("rb"/"wb"), no decoding happens at all — you get and give rawbytesobjects, which is required for non-text files like images.
Common Mistakes
1. Forgetting to close the file
Opening a file with plain file = open("data.txt", "w") and never calling file.close() can leave writes stuck in the buffer, never reaching disk, and it leaks an OS file handle for as long as the program runs. Always prefer with open("data.txt", "w") as file: — it closes the file automatically, even if an exception is raised inside the block.
2. Using "w" when you meant "a"
Opening an existing file with "w" mode immediately erases its entire contents, even before you write a single byte. A very common bug is repeatedly running a script that opens a log file with "w" inside a loop, wiping out everything written in previous iterations. If you want to add to a file without erasing it, use "a" mode instead.
3. Skipping the encoding argument
Calling open("notes.txt") without an encoding argument relies on the operating system’s default encoding, which differs between Windows, macOS, and Linux. A file containing non-ASCII characters (accented letters, emoji, curly quotes) that was written and read on different platforms can raise a UnicodeDecodeError or silently produce garbled text. Always pass encoding="utf-8" explicitly for text files.
Best Practices
- Always use
with open(...) as file:instead of manually callingopen()andclose(). - Always pass
encoding="utf-8"explicitly when opening text files. - Use
"x"mode when you want to guarantee you’re creating a brand-new file and never accidentally overwrite one. - Iterate over a file object directly (
for line in file:) instead of callingreadlines()when processing large files, to avoid loading the whole file into memory. - Prefer
pathlib.Pathobjects over raw strings for file paths in larger projects —Path("data.txt").read_text(encoding="utf-8")is a concise shortcut for small reads. - Use binary mode (
"rb"/"wb") for non-text files such as images, audio, or zip archives — never text mode. - Catch
FileNotFoundErrorwhen reading a file whose existence isn’t guaranteed, rather than checking existence separately and risking a race condition.
Practice Exercises
- Exercise 1: Write a program that creates a file called
shopping_list.txt, writes five grocery items to it (one per line), then reopens the file and prints each item prefixed with a bullet point (-). - Exercise 2: Write a program that reads a text file and counts how many lines it contains, how many words it contains, and how many characters it contains — similar to the Unix
wccommand. Print all three counts. - Exercise 3: Write a program that opens a log file in append mode and adds a new line each time it runs, then reads the whole file back and prints only the last 3 lines added. (Hint: read all lines into a list, then use slicing like
lines[-3:].)
Summary
open(file, mode)returns a file object used to read from or write to a file.- Always use the
withstatement so the file is closed automatically, even on errors. "r"reads,"w"writes and truncates,"a"appends, and"x"creates exclusively; add"b"for binary mode.read()loads the whole file as one string;readlines()returns a list of lines; iterating over the file object reads it line by line efficiently.write()writes a single string;writelines()writes a list of strings, and neither adds newline characters automatically.- Always specify
encoding="utf-8"for text files to avoid platform-dependent bugs. - Buffering means writes may not hit disk until the file is closed or flushed — another reason to always close files properly.
