In Python, the open function is the primary way to interact with files on the
filesystem. It returns a file object that provides methods for reading,
writing, and navigating file content. File I/O is fundamental for data
persistence, logging, configuration, and many other programming tasks.
The open function works with both text and binary data, supports a variety of
access modes, and can handle encodings, buffering, and error strategies. Used
together with the context manager (with statement), it ensures that files are
properly closed after use.
The simplest use of open requires a file path and a mode. The default mode is
'r' (read, text mode).
#!/usr/bin/python
# simplest_open.py
f = open('example.txt', 'r')
content = f.read()
f.close()
print(content)It is good practice to always close a file when you are finished with it,
otherwise operating system resources may be leaked.
The mode string controls how the file is opened. Common modes are:
| Mode | Meaning |
|---|---|
'r' |
read (default) |
'w' |
write (overwrites) |
'a' |
append |
'x' |
exclusive creation |
'r+' |
read and write |
'w+' |
write and read (overwrites) |
'a+' |
append and read |
A 'b' suffix (e.g. 'rb', 'wb') opens the file in binary mode. A 't'
suffix forces text mode (the default).
#!/usr/bin/python
# modes.py
# Write a new file (overwrites if exists)
with open('data.txt', 'w') as f:
f.write('First line\n')
# Append to the file
with open('data.txt', 'a') as f:
f.write('Second line\n')
# Exclusive creation – fails if file already exists
try:
with open('unique.txt', 'x') as f:
f.write('Created!\n')
except FileExistsError:
print('unique.txt already exists')Text mode ('t') decodes bytes to strings using a character encoding and
converts platform‑specific line endings to '\n' on reading and back on
writing. Binary mode ('b') reads and writes raw bytes without any
translation – useful for non‑text files like images or compressed data.
#!/usr/bin/python
# binary_read.py
with open('data.bin', 'wb') as f:
f.write(b'\x00\xFF\x00\xFF')
with open('data.bin', 'rb') as f:
raw = f.read()
print(raw) # b'\x00\xff\x00\xff'When reading or writing text, you should explicitly specify the encoding to
avoid platform‑dependent behaviour. The default encoding is usually UTF‑8,
but it can differ. The encoding parameter sets the desired character set.
#!/usr/bin/python
# encoding.py
text = 'Café – résumé'
# Write with explicit encoding
with open('cafe.txt', 'w', encoding='utf-8') as f:
f.write(text)
# Read back with the same encoding
with open('cafe.txt', 'r', encoding='utf-8') as f:
restored = f.read()
print(restored) # Café – résuméIf the file contains characters that cannot be encoded with the given encoding,
a UnicodeEncodeError will be raised. The errors parameter can be used to
control this behaviour (e.g., 'ignore', 'replace', 'xmlcharrefreplace').
Using open directly requires remembering to call .close(). The recommended
approach is to use the with statement, which acts as a context manager and
automatically closes the file at the end of the block, even if exceptions occur.
#!/usr/bin/python
# context_manager.py
# Preferred way
with open('numbers.txt', 'r') as f:
content = f.read()
print(content)
# File is automatically closed hereThis is more concise, safe, and idiomatic.
The .read() method reads the whole file content and returns it as a single
string (or bytes if in binary mode).
#!/usr/bin/python
# read_all.py
with open('poem.txt', 'r') as f:
whole_text = f.read()
print(repr(whole_text))For large files, reading everything at once may consume a lot of memory. In such
cases, iterate over the file line by line.
A file object is iterable and yields each line in turn. You can also use
.readline() to read one line at a time or .readlines() to get a list of all
lines.
#!/usr/bin/python
# read_lines.py
with open('quotes.txt', 'r') as f:
for line in f:
print(line.rstrip('\n'))
# Or with readlines()
with open('quotes.txt', 'r') as f:
lines = f.readlines()
print(lines)The iteration approach is memory efficient because only one line is held in
memory at a time.
The .write() method writes a string (or bytes) to the file. Use 'w' to
overwrite or 'a' to append. You can write multiple lines with .writelines().
#!/usr/bin/python
# write_file.py
data = ['apple', 'banana', 'cherry']
with open('fruits.txt', 'w') as f:
for item in data:
f.write(item + '\n')Note that .write() does not automatically add newline characters – you must
supply them if needed.
The .writelines() method accepts any iterable of strings and writes them all
at once without inserting separators. You are responsible for including any
newline characters in the strings.
#!/usr/bin/python
# writelines.py
lines = ['line one\n', 'line two\n', 'line three\n']
with open('output.txt', 'w') as f:
f.writelines(lines)
with open('output.txt', 'r') as f:
print(f.read())Using .writelines() can be more efficient than calling .write() in a loop
because it avoids repeated calls into the file object.
Every file object exposes a small set of read-only attributes that describe how
the file was opened. These are useful for debugging and for writing helper
functions that inspect their file arguments.
#!/usr/bin/python
# file_attrs.py
with open('sample.txt', 'w', encoding='utf-8') as f:
print(f.name) # 'sample.txt'
print(f.mode) # 'w'
print(f.encoding) # 'utf-8'
print(f.closed) # False
print(f.closed) # True – closed after the with blockThe closed attribute lets you check without risking an exception whether you
can still read from or write to the object.
Opening with 'a' preserves the existing content and adds new data at the end.
#!/usr/bin/python
# append_file.py
with open('log.txt', 'a') as f:
f.write('New log entry\n')If the file does not exist, it is created. The file pointer is positioned at the
end of the file, so subsequent reads (if 'a+' is used) need repositioning.
An optional integer argument to .read() limits the number of characters (or
bytes) read at once.
#!/usr/bin/python
# read_chunk.py
with open('big_file.bin', 'rb') as f:
while chunk := f.read(4096):
# process the 4 KiB chunk
print(len(chunk))This is essential for efficiently processing very large files.
When you need to process or extract only certain lines by line number, wrap the
file iterator with enumerate to track the current position.
#!/usr/bin/python
# read_specific_lines.py
target_lines = {0, 2, 4} # 0-based line numbers
with open('data.txt', 'r') as f:
for number, line in enumerate(f):
if number in target_lines:
print(f"Line {number}: {line.rstrip()}")Because the file is read one line at a time, memory usage stays low even for
very large files.
The .truncate(size) method resizes the file to at most size bytes (or
characters in text mode). If size is omitted, the file is truncated at the
current position. The file pointer is not moved automatically.
#!/usr/bin/python
# truncate.py
with open('draft.txt', 'w') as f:
f.write('Hello, World! This is extra content.')
with open('draft.txt', 'r+') as f:
f.truncate(13) # keep only 'Hello, World!'
with open('draft.txt', 'r') as f:
print(f.read()) # Hello, World!Truncation is especially useful when updating a file in place without
rewriting it entirely.
Buffered writes may not reach the disk immediately. Call .flush() to force
all pending data to be written to the OS, without closing the file. This is
useful in long-running scripts or when another process needs to read what has
just been written.
#!/usr/bin/python
# flush.py
import time
with open('progress.log', 'w') as f:
for step in range(1, 4):
f.write(f'Step {step} complete\n')
f.flush() # visible to other readers immediately
time.sleep(0.1) # simulate workUnder normal circumstances .flush() is not necessary because the with
block calls .close(), which flushes automatically.
The .seek(offset, whence) method moves the file pointer to a new position.
whence can be 0 (start of file), 1 (current position), or 2 (end of
file). The .tell() method returns the current byte offset.
In text mode, only seeks relative to the beginning are allowed (except when
using zero offset). Binary mode supports all seek options.
#!/usr/bin/python
# seek.py
with open('data.bin', 'rb') as f:
print(f.tell()) # 0
f.seek(5)
print(f.tell()) # 5
f.seek(-2, 1) # move back 2 bytes from current position
print(f.tell()) # 3
f.seek(0, 2) # move to end
print(f.tell()) # file size in bytesWhen a file cannot be opened, Python raises an OSError subclass such as
FileNotFoundError, PermissionError, or IsADirectoryError. It is wise to
handle these exceptions gracefully.
#!/usr/bin/python
# error_handling.py
try:
with open('missing.txt', 'r') as f:
content = f.read()
except FileNotFoundError:
print("The file does not exist.")
except PermissionError:
print("You do not have permission to open this file.")
except OSError as e:
print(f"An OS error occurred: {e}")The with statement itself still closes the file properly even if an exception
occurs inside the block.
When writing in text mode, the newline parameter controls how universal
newline translation works. It defaults to None, which converts '\n' to the
system’s default line separator on output and translates any platform line
ending to '\n' on input. You can set it to '' to disable translation
(useful when working with CSV files).
#!/usr/bin/python
# newline.py
import csv
with open('report.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Name', 'Age'])
writer.writerow(['Alice', 30])This ensures the CSV module controls line endings completely.
The buffering parameter sets the buffering policy. -1 uses the default
buffering (typically line buffering for interactive text files, and a fixed
buffer size for others). 0 switches buffering off (only for binary mode), 1
selects line buffering (text mode only), and an integer >1 specifies a buffer
size in bytes.
#!/usr/bin/python
# buffering.py
# Unbuffered binary write
with open('unbuffered.bin', 'wb', buffering=0) as f:
f.write(b'\x01\x02')
# data is written immediatelyFor most uses, the default buffering is appropriate.
io.StringIO provides an in-memory text stream with the same interface as a
real file object. It is useful for testing, for building output without
touching the filesystem, or for passing file-like objects to functions that
expect them.
#!/usr/bin/python
# stringio.py
import io
buffer = io.StringIO()
buffer.write('first line\n')
buffer.write('second line\n')
# Rewind and read back
buffer.seek(0)
print(buffer.read())
# Use it anywhere a file object is accepted
buffer.seek(0)
for line in buffer:
print(line.rstrip())
buffer.close()The analogous class for binary data is io.BytesIO.
JSON is the most common format for storing structured data in text files. The
json module's dump/load functions accept file objects directly, so they
combine naturally with open.
#!/usr/bin/python
# json_file.py
import json
config = {
'host': 'localhost',
'port': 5432,
'debug': True,
'tags': ['db', 'primary'],
}
# Write JSON to a file
with open('config.json', 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
# Read it back
with open('config.json', 'r', encoding='utf-8') as f:
loaded = json.load(f)
print(loaded['host']) # localhost
print(loaded['port']) # 5432Always specify encoding='utf-8' when working with JSON so that non-ASCII
characters (e.g. in string values) are handled correctly on every platform.
The pathlib module provides an object‑oriented approach to file paths. Its
Path.open() method works exactly like the built‑in open function but is
often more convenient when you manipulate paths.
#!/usr/bin/python
# pathlib_open.py
from pathlib import Path
data_dir = Path('data')
data_dir.mkdir(exist_ok=True)
file_path = data_dir / 'notes.txt'
with file_path.open('w', encoding='utf-8') as f:
f.write('Remember the milk\n')
with file_path.open('r', encoding='utf-8') as f:
print(f.read())The Path.open() method delegates directly to the built‑in open.
The following script shows a real‑world use that combines several concepts:
reading line by line, filtering, and writing results to a new file.
#!/usr/bin/python
# filter_errors.py
input_path = 'app.log'
output_path = 'errors_only.log'
try:
with open(input_path, 'r', encoding='utf-8') as infile, \
open(output_path, 'w', encoding='utf-8') as outfile:
for line in infile:
if 'ERROR' in line:
outfile.write(line)
print(f"Filtered errors written to {output_path}")
except FileNotFoundError:
print(f"Cannot find {input_path}")
except OSError as e:
print(f"File error: {e}")This example opens two files simultaneously – one for reading and one for
writing – using the context manager to ensure both are properly closed.