Uncategorized

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 a Path object.
  • 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 with line.

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

Method or pattern | Return value | Best use case | Memory considerations

read() | One string containing the remaining contents | Small files that should be handled as a whole | Stores the remaining file contents in memory

readline() | One string containing the next line | Controlled, one-line-at-a-time reading | Uses little memory for each call

readlines() | A list of line strings | When a list of all lines is convenient | Stores all remaining lines in memory

for line in file | One line per loop iteration | Large files and streaming line processing | Generally memory-efficient

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.

Mode | Primary use | File must already exist | Existing content behavior | Read allowed | Write allowed

r | Read text | Yes | Preserved | Yes | No

w | Replace or create text | No | Existing content is truncated immediately | No | Yes

a | Append text | No | Existing content is preserved; writes go to the end | No | Yes

x | Create a new text file only | No | Fails if the file already exists | No | Yes

r+ | Read and write an existing file | Yes | Preserved until your writes change it | Yes | Yes

w+ | Replace or create, then read and write | No | Existing content is truncated immediately | Yes | Yes

a+ | Append and read | No | Existing content is preserved; writes go to the end | Yes | Yes

rb | Read binary data | Yes | Preserved | Yes, as bytes | No

wb | Replace or create binary data | No | Existing content is truncated immediately | No | Yes

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

Symptom or exception | Likely cause | Recommended response

FileNotFoundError | The name, path, working directory, or capitalization is wrong | Check Path.cwd(), correct the path, or create the file when appropriate

PermissionError | The process lacks access or the location is protected | Use an accessible location or adjust permissions when authorized

UnicodeDecodeError | The selected encoding does not match the file bytes | Determine and specify the correct encoding, or use binary mode for raw data

Unexpectedly empty read result | The file cursor is already at the end | Store the first result, reopen the file, or call seek(0)

Existing contents disappeared after writing | The file was opened with w or w+ | Use a for additions or write changes to a separate output file

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 \n explicitly when you need line breaks.
  • Use writelines() for multiple strings, but add newline characters yourself.
  • Use w to replace, a to append, x to create only if absent, and r to read.
  • Assume w and w+ will truncate existing files.
  • Use rb and wb for 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, and UnicodeDecodeError.

Exam-relevant notes

  • The with statement 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.
  • w creates or truncates; a preserves existing content and appends; x fails 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.