To read a binary file in Python, open it with mode "rb" and call read(). You get a bytes object instead of a string, and nothing is decoded or changed along the way:
with open("orders.bin", "rb") as f:
data = f.read() # bytes
Every snippet here ran on Python 3.12.5 and NumPy 2.5.3, against one small file of four 2026 orders with totals in US dollars.
We’ll build that file, read it whole and in chunks, decode its records with struct, jump around with seek(), and finish by loading it into NumPy.
Create a sample binary file to read
Binary files are easier to understand when you know exactly what’s inside. This script writes orders.bin: a 10-byte header followed by fixed-size order records, each holding an ID, a date, a quantity and a dollar total.
import os
import struct
from datetime import date
orders = [
(10041, date(2026, 10, 1), 2, 59.98),
(10042, date(2026, 10, 3), 1, 149.99),
(10043, date(2026, 10, 7), 5, 24.75),
(10044, date(2026, 10, 9), 3, 312.40),
]
HEADER = struct.Struct("<4sHI") # magic, version, record count
RECORD = struct.Struct("<IIHd") # order id, yyyymmdd, quantity, total in USD
with open("orders.bin", "wb") as f:
f.write(HEADER.pack(b"\x89ORD", 1, len(orders)))
for oid, day, qty, total in orders:
f.write(RECORD.pack(oid, int(day.strftime("%Y%m%d")), qty, total))
print(HEADER.size, "byte header +", RECORD.size, "bytes per record")
print("orders.bin is", os.path.getsize("orders.bin"), "bytes")
Output:
10 byte header + 18 bytes per record
orders.bin is 82 bytes
Writing uses mode "wb", and struct.pack() turns Python values into raw bytes. The with block closes the file for you; closing a file in Python explains why that matters.
Read a binary file in Python with open() and rb mode
Opening in "rb" returns a bytes object. It looks like a string when printed, but indexing it gives integers from 0 to 255:
from pathlib import Path
with open("orders.bin", "rb") as f:
data = f.read()
print(type(data), len(data), "bytes")
print(data[:10]) # header as a bytes literal
print(data[:10].hex(" ")) # the same 10 bytes in hex
print(data[0], data[1:4]) # an index gives an int, a slice gives bytes
print(Path("orders.bin").read_bytes() == data)
Output:
<class 'bytes'> 82 bytes
b'\x89ORD\x01\x00\x04\x00\x00\x00'
89 4f 52 44 01 00 04 00 00 00
137 b'ORD'
True

hex(" ") makes the header readable, and one index returns a plain integer.Path.read_bytes() from pathlib does the same open, read and close in one call. I reach for it when the file is small and I don’t need a loop.
What happens if you open a binary file in text mode?
Text mode tries to decode the bytes with an encoding. Our header starts with byte 0x89, which isn’t valid UTF-8, so the read fails straight away:
try:
with open("orders.bin", encoding="utf-8") as f:
f.read()
except UnicodeDecodeError as err:
print("Text mode failed:", err)
with open("orders.bin", "rb") as f:
print("Binary mode read", len(f.read()), "bytes")
Output:
Text mode failed: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte
Binary mode read 82 bytes
Even when text mode doesn’t crash, it can quietly change line endings on Windows and corrupt the data. Use text mode only for real text, as in reading a file as a string in Python.
Read a large binary file in chunks
read() with no argument loads the whole file into memory. For big files, pass a size and loop until you get an empty bytes object. The walrus operator keeps that while loop short:
import hashlib
with open("orders.bin", "rb") as f:
while chunk := f.read(32):
print(f"{len(chunk):>2} bytes: {chunk[:8].hex(' ')} ...")
sha = hashlib.sha256()
with open("orders.bin", "rb") as f:
for block in iter(lambda: f.read(65536), b""):
sha.update(block)
with open("orders.bin", "rb") as f:
same = hashlib.file_digest(f, "sha256").hexdigest() == sha.hexdigest()
print("SHA-256:", sha.hexdigest()[:16], "... file_digest matches:", same)
Output:
32 bytes: 89 4f 52 44 01 00 04 00 ...
32 bytes: 8b 28 35 01 01 00 48 e1 ...
18 bytes: 3c 27 00 00 91 28 35 01 ...
SHA-256: aac0f344f43e9a23 ... file_digest matches: True

The last chunk is usually shorter, so never assume every chunk has the full size. Splitting data you already hold is a different job; see splitting a list into evenly sized chunks.
Since Python 3.11, hashlib.file_digest() handles that chunked hashing loop for you.
Decode binary records with the struct module
Raw bytes only become useful when you decode them. struct uses a format string: < means little-endian, 4s is four bytes, H a 2-byte unsigned int, I a 4-byte one, and d an 8-byte float.
import struct
from datetime import datetime
HEADER = struct.Struct("<4sHI")
RECORD = struct.Struct("<IIHd")
with open("orders.bin", "rb") as f:
magic, version, count = HEADER.unpack(f.read(HEADER.size))
print(magic, "version", version, "with", count, "orders")
body = f.read(count * RECORD.size)
total_sales = 0
for oid, ymd, qty, total in RECORD.iter_unpack(body):
day = datetime.strptime(str(ymd), "%Y%m%d").date()
print(f"#{oid} {day:%b %d, %Y} qty {qty} ${total:,.2f}")
total_sales += total
print(f"Total sales: ${total_sales:,.2f}")
Output:
b'\x89ORD' version 1 with 4 orders
#10041 Oct 01, 2026 qty 2 $59.98
#10042 Oct 03, 2026 qty 1 $149.99
#10043 Oct 07, 2026 qty 5 $24.75
#10044 Oct 09, 2026 qty 3 $312.40
Total sales: $547.12

HEADER.unpack() reads the magic bytes and count, and iter_unpack() turns each 18-byte record into an order with a dollar total.A struct.Struct object compiles the format once and gives you .size, so you never count bytes by hand. iter_unpack() needs the buffer length to be an exact multiple of that size.
Why does byte order matter when reading binary data?
The same four bytes mean different numbers depending on byte order. Read little-endian data as big-endian and you’ll get garbage that still looks like a valid number:
import struct
raw = (10041).to_bytes(4, "little")
print(raw.hex(" "))
print("little-endian:", struct.unpack("<I", raw)[0])
print("big-endian: ", struct.unpack(">I", raw)[0])
print("int.from_bytes:", int.from_bytes(raw, "little"))
Output:
39 27 00 00
little-endian: 10041
big-endian: 958857216
int.from_bytes: 10041
Match the format to whatever wrote the file; most Windows and Linux tools use little-endian. For a single integer, int.from_bytes() is enough. To see the bits themselves, try the Python bin() function.
Jump to a record with seek() and tell()
Fixed-size records let you skip straight to the one you want. seek(offset) moves the file position, tell() reports it, and seek(-8, 2) counts back from the end:
import struct
RECORD = struct.Struct("<IIHd")
HEADER_SIZE = 10
def read_order(path, n):
with open(path, "rb") as f:
f.seek(HEADER_SIZE + n * RECORD.size)
print("reading at byte", f.tell())
return RECORD.unpack(f.read(RECORD.size))
print(read_order("orders.bin", 2))
with open("orders.bin", "rb") as f:
f.seek(-8, 2) # 8 bytes before the end
print("last total:", struct.unpack("<d", f.read())[0])
Output:
reading at byte 46
(10043, 20261007, 5, 24.75)
last total: 312.4
Seeking to arbitrary offsets only works reliably in binary mode, which is one more reason to use "rb".
Read binary data into a buffer with readinto()
readinto() fills a buffer you already own instead of creating a new bytes object each time. With a bytearray, you can also edit the data in place:
import os
buf = bytearray(os.path.getsize("orders.bin"))
with open("orders.bin", "rb") as f:
n = f.readinto(buf)
print(n, "bytes read into a", type(buf).__name__)
buf[4:6] = (2).to_bytes(2, "little") # a bytearray can be changed in place
print(buf[:10].hex(" "))
Output:
82 bytes read into a bytearray
89 4f 52 44 02 00 04 00 00 00
There’s a full walk-through of this pattern in reading a binary file into a byte array in Python, and the bytearray() function covers the type itself.
Load a binary file into a NumPy array with fromfile()
When a file holds thousands of identical records, NumPy beats a Python loop. Describe one record as a structured dtype, skip the header with offset, and every field becomes a column:
import numpy as np
order = np.dtype([("order_id", "<u4"), ("ymd", "<u4"), ("qty", "<u2"), ("total", "<f8")])
arr = np.fromfile("orders.bin", dtype=order, offset=10)
print(arr.shape, order.itemsize, "bytes per record")
print(arr["order_id"])
print(arr["total"], "sum:", round(float(arr["total"].sum()), 2))
print("qty >= 3:", arr[arr["qty"] >= 3]["order_id"])
Output:
(4,) 18 bytes per record
[10041 10042 10043 10044]
[ 59.98 149.99 24.75 312.4 ] sum: 547.12
qty >= 3: [10043 10044]

np.fromfile() maps each 18-byte record to a row, so you can sum the totals and filter by quantity without a loop.Use np.frombuffer() if you already have the bytes in memory. For text-based data files, reading a file into an array in Python is the better fit.
Ways to read a binary file in Python compared
| Method | Returns | Best for |
|---|---|---|
f.read() | bytes | Small files, whole content |
f.read(n) in a loop | bytes chunks | Large files, hashing, streaming |
struct.unpack() | tuple of values | Headers and fixed records |
f.seek() + read() | bytes | Random access to one record |
f.readinto(buf) | count of bytes | Reusing a buffer, editing data |
np.fromfile() | NumPy array | Many identical numeric records |
If binary files are part of your project, these guides pair well with this one:
- Save an array to a file in Python
- Write an array to a file in Python
- The len() function in Python
- Python for loop with index