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: calls file.__enter__() at the start of the block and guarantees file.__exit__() — which calls close() — runs when the block ends, whether it ends normally or via an exception. This is equivalent to a try/finally that 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 with file.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 specified encoding. In binary mode ("rb"/"wb"), no decoding happens at all — you get and give raw bytes objects, 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 calling open() and close().
  • 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 calling readlines() when processing large files, to avoid loading the whole file into memory.
  • Prefer pathlib.Path objects 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 FileNotFoundError when 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 wc command. 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 with statement 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.