Reading and Writing Files with Python's with Statement
Learn how to safely read, write, append, and process text and binary files in Python using open() and the with statement.
Python programs often need to load settings, save results, process logs, or copy data. The open() function gives your program access to a file, while the with statement makes sure that the file is cleaned up correctly afterward.
This lesson covers text and binary files, file modes, encodings, paths, the file cursor, common exceptions, and safe file-update patterns.
Why use the with statement?
The with statement is a Python context-management construct. It enters a managed block, runs the code inside that block, and performs cleanup when the block ends. A file is one example of a resource that should be cleaned up.
A file opened by open() is represented by a file object. The variable assigned after as is often called a file handle, although it is technically a file object.
with open("notes.txt", "r", encoding="utf-8") as file:
contents = file.read()
print(contents)
When Python leaves the indented block, it closes the file automatically. This also happens if an exception is raised inside the block. Automatic cleanup prevents files from remaining open and makes the program easier to reason about.
Conceptual comparison with manual closing
Without with, you must remember to close the file yourself:
file = open("notes.txt", "r", encoding="utf-8")
try:
contents = file.read()
finally:
file.close()
This pattern can work, but it is easier to forget close(). The with statement expresses the same cleanup rule more clearly. The object used with with is called a context manager: it defines setup and cleanup behavior for the managed block.
Basic file-opening syntax
The general pattern is:
with open(path, mode) as file_handle:
operations_using_file_handle()
path: The file's location, such as"data/notes.txt"or aPathobject.mode: A string controlling whether the file is read, replaced, appended to, created, or treated as binary data.file_handle: The file object used for reading or writing.- Indented block: The operations that need the open file belong under the
withline.
For example:
with open("notes.txt", "r", encoding="utf-8") as notes_file:
first_line = notes_file.readline()
print(first_line)
Python opens the file before entering the block. When the block ends, Python closes it, so this is safe even if later code raises an exception inside the block.
Reading text files
Text mode reads characters and returns Python strings. Reading is normally done with mode "r", which is also the default mode for open().
Read the entire file with read()
read() returns the remaining contents as one string. It is convenient for small files such as a short notes file or configuration document.
with open("notes.txt", "r", encoding="utf-8") as notes_file:
contents = notes_file.read()
print(contents)
After this call, the file cursor is at the end of the file. The file cursor is the current position from which the next read or write occurs.
Read one line with readline()
readline() returns the next line as a string. If the line in the file ends with a newline, that newline is usually included in the returned string.
with open("names.txt", "r", encoding="utf-8") as names_file:
first_name = names_file.readline()
second_name = names_file.readline()
print(first_name.strip())
print(second_name.strip())
At the end of the file, readline() returns an empty string.
Read all lines with readlines()
readlines() returns a list containing the remaining lines:
with open("names.txt", "r", encoding="utf-8") as names_file:
lines = names_file.readlines()
for line in lines:
print(line.strip())
Each list item commonly retains its ending newline. Because all lines are stored in a list, this method can use substantial memory for a large file.
Iterate directly over the file
A file object can be used directly in a for loop. Python obtains one line at a time, making this a good default for logs, name lists, and other potentially large files.
with open("names.txt", "r", encoding="utf-8") as names_file:
for line in names_file:
cleaned_line = line.strip()
if cleaned_line:
print(cleaned_line)
strip() removes whitespace from both ends. If you only want to remove line-ending characters while preserving spaces at the edges, use rstrip("\n"). On text files, rstrip("\r\n") can remove either common line-ending style.
Common reading methods
The file cursor and repeated reads
Read operations advance the cursor. A second call to read() usually returns an empty string because the cursor is already at the end:
with open("notes.txt", "r", encoding="utf-8") as notes_file:
first_result = notes_file.read()
second_result = notes_file.read()
print(first_result) # The file contents
print(second_result) # ""
If you need to read the same open file again, move the cursor back with seek(0):
with open("notes.txt", "r", encoding="utf-8") as notes_file:
first_result = notes_file.read()
notes_file.seek(0)
second_result = notes_file.read()
For simple independent operations, storing the first result or reopening the file is often clearer than repositioning the cursor.
Writing text files
In text mode, write() expects a string and returns the number of characters written. It does not automatically add a newline.
with open("report.txt", "w", encoding="utf-8") as report_file:
report_file.write("Daily report\n")
report_file.write("Items processed: 12\n")
If a value is an integer or another non-string object, convert or format it first:
items_processed = 12
with open("report.txt", "w", encoding="utf-8") as report_file:
report_file.write(f"Items processed: {items_processed}\n")
report_file.write("Total: " + str(items_processed) + "\n")
Writing multiple strings with writelines()
writelines() writes each string from an iterable, such as a list. It does not insert newline characters automatically.
lines = ["red\n", "green\n", "blue\n"]
with open("colors.txt", "w", encoding="utf-8") as colors_file:
colors_file.writelines(lines)
Without the explicit \n characters, the result would be redgreenblue rather than three separate lines.
File modes
A file mode is a string that tells open() what operations are allowed and what should happen to existing content.
Read mode: r
Use r when the target must already exist and your program only needs to read it.
with open("config.txt", "r", encoding="utf-8") as config_file:
configuration = config_file.read()
Write mode: w
Use w to create a file if it does not exist or replace the contents of an existing file.
with open("report.txt", "w", encoding="utf-8") as report_file:
report_file.write("A new report\n")
Append mode: a
Use a for journals, logs, and other files where each operation adds information without deleting earlier entries.
entry = "User signed in\n"
with open("activity.log", "a", encoding="utf-8") as log_file:
log_file.write(entry)
Control the newline yourself. If the existing file does not end with a newline, simply writing one may join the new entry to the previous text. For structured logs, define a consistent line format and newline policy.
Exclusive creation mode: x
Use x when overwriting an existing file would be unsafe. Opening with this mode raises FileExistsError if the target already exists.
with open("first-run.txt", "x", encoding="utf-8") as new_file:
new_file.write("Created only if absent\n")
Read/write variants
The plus sign adds the opposite operation: r+ reads and writes an existing file, w+ creates or truncates before allowing both operations, and a+ allows reading while preserving append behavior for writes.
When mixing reads and writes, pay attention to the cursor. Use methods such as seek() when you deliberately need to change positions, and do not assume that a write automatically places the cursor where a later read should begin.
Binary modes
Adding b selects binary mode. Binary operations return or accept bytes, not text strings. Use binary modes for images, audio, archives, and other data that should not be decoded as text.
with open("original-image.png", "rb") as source_file:
image_data = source_file.read()
with open("backup-image.png", "wb") as backup_file:
backup_file.write(image_data)
Do not pass encoding="utf-8" to a binary-mode operation. Encoding applies when Python converts between bytes and text.
Encoding and newline handling
An encoding is a character representation. When reading, Python decodes file bytes into strings. When writing, Python encodes strings into bytes. Explicitly specifying encoding="utf-8" makes the intended text encoding clear and improves portability across operating systems and environments.
with open("messages.txt", "r", encoding="utf-8") as messages_file:
messages = messages_file.read()
with open("messages-copy.txt", "w", encoding="utf-8") as output_file:
output_file.write(messages)
Text files contain line-ending characters. In Python source code, the usual newline character is written as \n. A line read by readline() or direct iteration often includes its ending newline, so writing that line and adding another newline can create blank lines.
with open("input.txt", "r", encoding="utf-8") as input_file:
with open("cleaned.txt", "w", encoding="utf-8") as output_file:
for line in input_file:
cleaned = line.strip()
if cleaned:
output_file.write(cleaned + "\n")
Use strip() when surrounding whitespace is not meaningful. Use rstrip("\n") when spaces should be preserved but the line terminator should be removed.
Relative and absolute paths
A relative path is interpreted from the program's current working directory. For example, "data/notes.txt" means a data folder inside the current working directory.
An absolute path identifies a location from the filesystem root. Its exact form depends on the operating system. Relative paths are usually easier to move between computers, but they depend on where the program is started.
from pathlib import Path
current_folder = Path.cwd()
print(current_folder)
Path.cwd() shows the current working directory. If a relative path causes FileNotFoundError, check this directory first.
Build paths with pathlib
pathlib.Path provides a readable, cross-platform way to construct paths. The slash operator joins path components using the appropriate platform rules.
from pathlib import Path
path = Path("data") / "filename.txt"
with open(path, "r", encoding="utf-8") as file:
contents = file.read()
You can also use Path.open():
from pathlib import Path
path = Path("data") / "filename.txt"
with path.open("r", encoding="utf-8") as file:
contents = file.read()
The path still depends on the current working directory when it is relative. A Path object improves construction and readability; it does not change how relative paths are interpreted.
Error handling
File operations can fail because a path is wrong, access is restricted, or the file's bytes do not match the selected text encoding. Use targeted exception handlers and provide a useful response.
Handle a missing file
try:
with open("settings.txt", "r", encoding="utf-8") as settings_file:
settings = settings_file.read()
except FileNotFoundError:
print("The settings file was not found. Check its path or create it first.")
FileNotFoundError can result from a misspelled name, an incorrect relative path, a different current working directory, or capitalization that does not match the filesystem.
Handle inaccessible locations
try:
with open("protected/report.txt", "w", encoding="utf-8") as report_file:
report_file.write("Report\n")
except PermissionError:
print("You do not have permission to write to that location.")
PermissionError means the process cannot perform the requested operation. Select a location where the program has access, adjust permissions only when authorized, and consider whether another application is restricting the file.
Handle an incorrect text encoding
try:
with open("unknown-source.txt", "r", encoding="utf-8") as source_file:
text = source_file.read()
except UnicodeDecodeError:
print("The file is not encoded as expected; determine its encoding first.")
A UnicodeDecodeError means the file bytes could not be decoded using the selected encoding. Determine the source encoding and pass the matching value. Use binary mode only when the program should process raw bytes rather than text.
The with statement still performs its cleanup when code in the block raises an exception. The exception may then be caught outside the block, as in these examples.
Common file problems
Safe file-update patterns
Append instead of replace when adding data
Choose append mode when earlier entries must remain. Choose write mode when the file is a generated result that should be rebuilt from scratch. Do not use w merely because the file already exists: opening it can erase its contents before the first write().
from datetime import datetime
entry = f"{datetime.now().isoformat()} - backup completed\n"
with open("backup.log", "a", encoding="utf-8") as log_file:
log_file.write(entry)
Generate a replacement report
Write mode is appropriate when the program is generating a complete report:
scores = {"Ada": 92, "Linus": 88}
with open("scores-report.txt", "w", encoding="utf-8") as report_file:
report_file.write("Scores\n")
for name, score in scores.items():
report_file.write(f"{name}: {score}\n")
This intentionally replaces any previous report.
Transform input into separate output
A safe transformation pattern reads one file and writes a different file. It avoids opening the input with write mode and destroying the source before it has been processed.
with open("source.txt", "r", encoding="utf-8") as input_file:
with open("uppercase.txt", "w", encoding="utf-8") as output_file:
for line in input_file:
output_file.write(line.upper())
The two with blocks can also be written on one line with separate context managers, but nested blocks are often easier for beginners to read. Both files are closed automatically when their blocks end.
Editing a file in place
In-place editing requires care. Opening the original file with w erases it immediately. For a reliable update, read the original, calculate the complete new content, and write to a separate temporary or output file. After verifying the result, a carefully designed replacement step can update the original.
with open("original.txt", "r", encoding="utf-8") as input_file:
updated_text = input_file.read().replace("old", "new")
with open("updated.txt", "w", encoding="utf-8") as output_file:
output_file.write(updated_text)
Writing to a separate file gives you the opportunity to inspect the result and retain the original if something goes wrong.
Reading and writing binary data
Binary files should be copied or processed as bytes without text decoding. The following example copies an image:
with open("photo.jpg", "rb") as input_file:
data = input_file.read()
with open("photo-copy.jpg", "wb") as output_file:
output_file.write(data)
For very large binary files, reading the entire file at once may use too much memory. A later step can process fixed-size byte chunks instead:
with open("source.bin", "rb") as input_file:
with open("copy.bin", "wb") as output_file:
while chunk := input_file.read(8192):
output_file.write(chunk)
Quick reference
- Use
with open(path, mode, encoding="utf-8") as file:for ordinary text files. - Keep all file operations inside the indented managed block.
- Use
read()for all contents,readline()for one line,readlines()for a list, and direct iteration for memory-efficient line processing. - Remember that reads advance the file cursor.
- Use
write()with strings and include\nexplicitly when you need line breaks. - Use
writelines()for multiple strings, but add newline characters yourself. - Use
wto replace,ato append,xto create only if absent, andrto read. - Assume
wandw+will truncate existing files. - Use
rbandwbfor bytes such as images. - Specify the text encoding explicitly, commonly with
encoding="utf-8". - Check the current working directory when relative paths fail.
- Use targeted handlers for
FileNotFoundError,PermissionError, andUnicodeDecodeError.
Exam-relevant notes
- The
withstatement closes a file automatically, including when an exception occurs inside its block. read()returns a string in text mode and bytes in binary mode.writelines()does not add newline characters.wcreates or truncates;apreserves existing content and appends;xfails if the file exists.- A relative path is resolved from the current working directory, not necessarily the folder containing the Python source file.
write()expects a string for a text-mode file, so format or convert numbers before writing.
For related Python fundamentals, see Obtaining the List Length. This lesson's canonical page is Reading and Writing with the with Statement.