Complete guide to Python's os.pwrite function covering file writing at specific offsets, low-level I/O operations, and practical examples.
Last modified April 11, 2025
This comprehensive guide explores Python’s os.pwrite function, which writes data to a file at a specific offset without changing the file pointer. We’ll cover its parameters, behavior, and practical use cases.
The os.pwrite function writes bytes to a file descriptor at a specified offset. It’s similar to os.write but doesn’t modify the file pointer position.
Key parameters: fd (file descriptor), data (bytes to write), offset (position in file). Returns number of bytes written. Requires file opened for writing.
This example demonstrates basic usage of os.pwrite to write data at a specific position in a file. We first create a file with some content.
basic_write.py
import os
with open(“data.txt”, “w”) as f: f.write(“Initial content”)
fd = os.open(“data.txt”, os.O_RDWR)
bytes_written = os.pwrite(fd, b"NEW “, 8) print(f"Wrote {bytes_written} bytes”)
os.lseek(fd, 0, os.SEEK_SET) print(os.read(fd, 100)) # Output: b’Initial NEWtent'
os.close(fd)
This writes “NEW " at position 8 in the file. The original content “content” becomes “NEWtent” because we overwrote part of it. The file pointer remains unchanged after pwrite.
Notice we use os.open to get a file descriptor, as pwrite requires a file descriptor, not a file object.
This example shows writing at multiple positions in a file while maintaining the original file pointer position between writes.
multiple_writes.py
import os
with open(“records.txt”, “w”) as f: f.write(“ABCDEFGHIJKLMNOPQRSTUVWXYZ”)
fd = os.open(“records.txt”, os.O_RDWR)
initial_pos = os.lseek(fd, 0, os.SEEK_CUR) print(f"Initial position: {initial_pos}”)
os.pwrite(fd, b"123", 5) # Overwrite positions 5-7 os.pwrite(fd, b"456", 10) # Overwrite positions 10-12 os.pwrite(fd, b"789", 20) # Overwrite positions 20-22
current_pos = os.lseek(fd, 0, os.SEEK_CUR) print(f"Position after writes: {current_pos}")
os.lseek(fd, 0, os.SEEK_SET) print(os.read(fd, 100)) # Output: b’ABCDE123HIJ456NOPQRST789VWXYZ'
os.close(fd)
Each pwrite operation writes at the specified offset without affecting the others or the file pointer. This allows precise modifications at known file locations.
The output shows the original string with our inserted number sequences at the specified positions.
While pwrite is typically for overwriting, we can use it to append by writing at the end of file. This requires knowing the file size.
append_write.py
import os
with open(“log.txt”, “w”) as f: f.write(“Log starts here\n”)
fd = os.open(“log.txt”, os.O_RDWR)
file_size = os.lseek(fd, 0, os.SEEK_END)
os.pwrite(fd, b"Entry 1\n", file_size) file_size += len(“Entry 1\n”)
os.pwrite(fd, b"Entry 2\n", file_size) file_size += len(“Entry 2\n”)
os.pwrite(fd, b"Entry 3\n", file_size)
os.lseek(fd, 0, os.SEEK_SET) print(os.read(fd, 1000).decode())
os.close(fd)
This demonstrates how to append data using pwrite by tracking the file size. Each write goes at the end of the current content.
While os.write with O_APPEND is simpler for pure appending, this shows pwrite’s flexibility.
pwrite may write fewer bytes than requested. This example shows how to handle partial writes and ensure complete data is written.
partial_writes.py
import os
fd = os.open(“partial.dat”, os.O_RDWR | os.O_CREAT)
large_data = b"X" * 1000000 # 1MB of data offset = 0 remaining = len(large_data)
while remaining > 0: written = os.pwrite(fd, large_data[-remaining:], offset) if written == 0: # Disk full? raise IOError(“Failed to write data”) offset += written remaining -= written print(f"Wrote {written} bytes, {remaining} remaining")
os.close(fd)
print(f"Final file size: {os.path.getsize(‘partial.dat’)} bytes")
This implements a write loop that continues until all data is written. It updates the offset and remaining data with each partial write.
In practice, disk full situations are rare, but robust code should handle partial writes for large data transfers.
pwrite is atomic when the writes are within a single filesystem block, making it useful for thread-safe operations. This example demonstrates this property.
thread_safe.py
import os import threading
fd = os.open(“counter.txt”, os.O_RDWR | os.O_CREAT) os.write(fd, b"0") # Initialize counter os.lseek(fd, 0, os.SEEK_SET)
def increment_counter(thread_id): for _ in range(1000): # Read current value current = int(os.pread(fd, 1, 0)) # Write incremented value os.pwrite(fd, str(current + 1).encode(), 0)
threads = [] for i in range(10): t = threading.Thread(target=increment_counter, args=(i,)) threads.append(t) t.start()
for t in threads: t.join()
os.lseek(fd, 0, os.SEEK_SET) print(f"Final counter value: {os.read(fd, 10).decode()}")
os.close(fd)
This implements a simple counter with concurrent increments. While not perfect (race conditions exist between read and write), it demonstrates pwrite’s atomic write capability.
For true atomic increments, consider file locking or other synchronization methods in addition to pwrite.
pwrite excels at binary file manipulation. This example shows modifying specific bytes in a binary file while leaving others unchanged.
binary_edit.py
import os
with open(“data.bin”, “wb”) as f: f.write(bytes(range(256))) # 0x00 to 0xFF
fd = os.open(“data.bin”, os.O_RDWR)
os.pwrite(fd, b"\xFF\xFF", 0x10) # Overwrite positions 0x10-0x11 os.pwrite(fd, b"\xAA\xBB", 0x80) # Overwrite positions 0x80-0x81
os.lseek(fd, 0, os.SEEK_SET) content = os.read(fd, 256)
print(f"Byte at 0x10: {hex(content[0x10])}") print(f"Byte at 0x11: {hex(content[0x11])}") print(f"Byte at 0x80: {hex(content[0x80])}") print(f"Byte at 0x81: {hex(content[0x81])}")
os.close(fd)
This creates a binary file with a known pattern, then modifies specific byte ranges using pwrite. The rest of the file remains unchanged.
This technique is useful for binary file formats where you need to modify specific headers or data structures within a larger file.
This example compares pwrite performance with regular write when doing random access to a large file.
performance.py
import os import time import random
def test_pwrite(fd, positions): start = time.time() for pos in positions: os.pwrite(fd, b"X", pos) return time.time() - start
def test_write(fd, positions): start = time.time() for pos in positions: os.lseek(fd, pos, os.SEEK_SET) os.write(fd, b"X") return time.time() - start
fd = os.open(“perf_test.dat”, os.O_RDWR | os.O_CREAT) os.ftruncate(fd, 1000000) # 1MB file
positions = [random.randint(0, 999999) for _ in range(1000)]
pwrite_time = test_pwrite(fd, positions)
write_time = test_write(fd, positions)
os.close(fd)
print(f"pwrite time: {pwrite_time:.4f} seconds") print(f"write time: {write_time:.4f} seconds") print(f"Ratio: {write_time/pwrite_time:.2f}x")
This benchmarks writing to 1000 random positions in a 1MB file. pwrite typically performs better by avoiding repeated seek operations.
The performance difference grows with more random writes, making pwrite ideal for certain database-like operations.
File descriptors: Requires proper file descriptor management
Atomic writes: Writes up to PIPE_BUF bytes are atomic
Error handling: Always check return value for partial writes
Concurrency: Safe for multi-threaded use with proper coordination
Platform support: Available on Unix-like systems, not Windows
Use for random access: Ideal for writing at known offsets
Check return values: Verify all bytes were written
Combine with pread: For read-modify-write cycles
Consider file sync: Use fsync for durability when needed
Prefer for large files: More efficient than seek+write
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all Python tutorials.