Binary Data with struct
Reviewed & published by Brayan K
The struct module converts between Python values and compact C-style bytes, letting you pack numbers into binary and unpack raw bytes from files, packets, and devices.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
When you need to read a binary file header or speak a wire protocol, struct is the precise, byte-accurate tool — a tiny format string describes exactly how the bytes are laid out.
What You'll Learn in This Lesson
- Packing values into bytes with struct.pack
- Unpacking bytes back into Python values
- Format characters: i, h, f, s and repeats
- Byte order with < > ! = (endianness)
- Measuring layouts with struct.calcsize
- Reading a real binary file header
1 pack & unpack: Values ⇄ Bytes
struct.pack(fmt, *values) writes values into a bytes object; struct.unpack(fmt, data) reads them back as a tuple. The format string describes the layout — here >hhl means two shorts and a long, all big-endian:
import struct
# Pack two shorts (h) and a long (l), big-endian (>)
packed = struct.pack(">hhl", 1, 2, 3)
print(packed)
# b'\x00\x01\x00\x02\x00\x00\x00\x03'
print(len(packed)) # 8 (2 + 2 + 4 bytes)
# Unpack always returns a tuple
nums = struct.unpack(">hhl", packed)
print(nums) # (1, 2, 3)import struct
# Pack 3 unsigned bytes (B) and one int (i), big-endian
packed = struct.pack(">BBBi", 255, 0, 128, 70000)
print("bytes:", len(packed))
r, g, b, value = struct.unpack(">BBBi", packed)
print("rgb:", r, g, b)
print("value:", value)
# ✅ Expected output:
# bytes: 7
# rgb: 255 0 128
# value: 700002 Byte Order & Format Characters
The first character of a format sets byte order. The same number 1 packs to different bytes depending on endianness, which is why you must always be explicit when parsing a real format:
import struct
# 4-byte int 1, two ways
little = struct.pack("<i", 1) # least significant byte first
big = struct.pack(">i", 1) # most significant byte first
print("little:", little) # b'\x01\x00\x00\x00'
print("big: ", big) # b'\x00\x00\x00\x01'
# Read each back with the matching order
print(struct.unpack("<i", little)[0]) # 1
print(struct.unpack(">i", big)[0]) # 1
# Floats use f (4 bytes) and d (8 bytes)
floats = struct.pack("<3f", 1.5, 2.5, 3.5)
print(struct.unpack("<3f", floats)) # (1.5, 2.5, 3.5)< little-endian > big-endian
! network (big-endian) = native, no paddingNetwork protocols and many file formats use big-endian (> or !). Pick one and stick with it across pack and unpack.
import struct
# Same value, opposite byte orders -> different bytes
value = 258 # = 0x0102
print("big :", struct.pack(">H", value))
print("little:", struct.pack("<H", value))
# Reading with the WRONG order flips the bytes
wrong = struct.unpack("<H", struct.pack(">H", value))[0]
print("misread:", wrong)
# ✅ Expected output:
# big : b'\x01\x02'
# little: b'\x02\x01'
# misread: 5133 Reading a Binary Header & calcsize
Real binary formats start with a fixed header: a magic signature, a version, and some counts. struct.calcsize(fmt) tells you how many bytes that header occupies, so you know exactly how much to read:
import struct
# Layout: 4-byte magic (4s), version (H), record count (I)
HEADER = ">4sHI"
print("header size:", struct.calcsize(HEADER)) # 10
# Build a header as if it were the start of a file
raw = struct.pack(HEADER, b"DATA", 2, 100)
print(raw) # b'DATA\x00\x02\x00\x00\x00d'
# Parse it back
magic, version, count = struct.unpack(HEADER, raw)
print("magic:", magic.decode("ascii")) # magic: DATA
print("version:", version) # version: 2
print("records:", count) # records: 100import struct
# A tiny 3-field record: id (H), temp as a float (f), flag (B)
RECORD = "<HfB"
print("record size:", struct.calcsize(RECORD))
raw = struct.pack(RECORD, 7, 21.5, 1)
rec_id, temp, flag = struct.unpack(RECORD, raw)
print("id:", rec_id)
print("temp:", temp)
print("flag:", bool(flag))
# Expected output:
# record size: 7
# id: 7
# temp: 21.5
# flag: True🎯 YOUR TURN: Fill in the Blanks
Fill in each ___ so the format string is big-endian and the round-trip prints the original values. (Hint: H = unsigned short, I = unsigned int.)
import struct
# Big-endian: one short then one int
FMT = "___HI" # replace ___ with the big-endian prefix
raw = struct.pack(FMT, 3, 90000)
print("size:", struct.calcsize(FMT))
# Replace ___ with the function that reads bytes back
year, total = struct.___(FMT, raw)
print("year:", year)
print("total:", total)
# Expected output:
# size: 6
# year: 3
# total: 90000Common Errors
❌ Wrong number of bytes for the format
struct.unpack(">i", b"\x00\x01")
# struct.error: unpack requires a buffer of 4 bytes✅ Match the buffer length to calcsize(fmt)
import struct
data = struct.pack(">i", 1) # exactly 4 bytes
print(struct.unpack(">i", data)) # (1,)❌ Treating unpacked bytes as a string
magic, = struct.unpack(">4s", b"DATA")
print(magic == "DATA") # False! magic is b'DATA', not 'DATA'✅ Decode bytes to text explicitly
magic, = struct.unpack(">4s", b"DATA")
print(magic.decode("ascii") == "DATA") # True❌ Forgetting the byte-order prefix
# No prefix = native order + padding -> sizes can surprise you
struct.calcsize("ci") # may be 8 (padding inserted), not 5✅ Add a prefix to get exact, portable sizes
import struct
print(struct.calcsize("<ci")) # 5 (no padding, predictable)🎯 Mini Challenge: Parse Sensor Records
A device emits a stream of fixed-size records: an id (H) and a reading (f), big-endian. Pack three of them, then loop through the bytes calcsize at a time to unpack each record.
import struct
RECORD = ">Hf"
size = struct.calcsize(RECORD)
# Build a "stream" of 3 records back-to-back
stream = b"".join([
struct.pack(RECORD, 1, 19.5),
struct.pack(RECORD, 2, 22.0),
struct.pack(RECORD, 3, 18.25),
])
print("record size:", size)
for i in range(0, len(stream), size):
chunk = stream[i:i + size]
rec_id, reading = struct.unpack(RECORD, chunk)
print(f"id={rec_id} reading={reading}")
# ✅ Expected output:
# record size: 6
# id=1 reading=19.5
# id=2 reading=22.0
# id=3 reading=18.25📚 Keep Learning
- struct — Interpret bytes as packed binary data (official docs)
- Format characters & byte-order table
- bytes and bytearray — the raw types struct works with
Frequently Asked Questions
Lesson complete — you speak binary now!
You can pack values into bytes and unpack them back, read format characters like i, h, f, and s, control endianness with < > ! =, use calcsize to measure layouts, and walk a binary stream record by record.
Practice quiz
What does the struct module do?
- Defines C-style structs as Python classes
- Compresses Python objects to disk
- Converts between Python values and C-style packed bytes
- Parses JSON into objects
Answer: Converts between Python values and C-style packed bytes. struct packs Python values into compact bytes and unpacks bytes back into values using a format string.
What does struct.unpack always return?
- A tuple, even for one value
- A single value
- A list
- A dictionary
Answer: A tuple, even for one value. unpack returns a tuple even for a single value: struct.unpack('>i', data) gives (5,), so index [0] or unpack it.
What does the '<' prefix mean in a format string?
- Big-endian
- Network byte order
- Native with padding
- Little-endian
Answer: Little-endian. '<' is little-endian (least significant byte first); '>' and '!' are big-endian, '=' is native order.
Which prefix means network byte order?
- <
- !
- =
- @
Answer: !. '!' is network byte order, which is the same as big-endian ('>').
What is the size of the format '>4sHI' according to struct.calcsize?
- 10
- 7
- 8
- 12
Answer: 10. 4s (4) + H (2) + I (4) = 10 bytes, since the explicit byte-order prefix removes padding.
What does the 'f' format character represent?
- A 2-byte short
- An 8-byte double
- A 4-byte float
- A flag bit
Answer: A 4-byte float. 'f' is a 4-byte float; 'd' is an 8-byte double, 'h' a 2-byte short, 'i' a 4-byte int.
Why do you get bytes back instead of a str from an 's' format?
- struct only works on numbers
- struct works at the byte level, so you decode text yourself
- It is a bug in struct
- The encoding was wrong
Answer: struct works at the byte level, so you decode text yourself. The 's' code yields a bytes object; decode it yourself with .decode('utf-8') or 'ascii' when you know the encoding.
What does struct.pack('>H', 258) produce?
- b'\x02\x01'
- b'\x00\x00'
- b'258'
- b'\x01\x02'
Answer: b'\x01\x02'. 258 is 0x0102; big-endian writes the most significant byte first, giving b'\x01\x02'.
What does struct.unpack('>i', b'\x00\x01') raise?
- Nothing — it returns (1,)
- A struct.error, because the buffer must be 4 bytes
- A TypeError
- A UnicodeDecodeError
Answer: A struct.error, because the buffer must be 4 bytes. The 'i' format needs exactly 4 bytes; a 2-byte buffer raises struct.error. Match the buffer to calcsize(fmt).
Why should you always include a byte-order prefix in a format string?
- It makes packing faster
- It is required syntax
- Without one, native order plus padding can give surprising sizes
- It enables compression
Answer: Without one, native order plus padding can give surprising sizes. No prefix means native byte order with alignment padding, so sizes vary by platform; a prefix gives exact, portable layouts.
Continue this course
- Previous: The operator Module
- Next: Text Wrapping & Formatting (textwrap) — Wrap, indent, dedent and shorten text cleanly with textwrap
- Quick reference: Python cheat sheet
Frequently asked questions
What does the struct module do?
struct converts between Python values and C-style packed bytes. struct.pack turns numbers and strings into a compact bytes object using a format string, and struct.unpack reads those bytes back into Python values. It's how you read and write binary file formats, network packets, and data from low-level hardware or other languages.
What do the < > ! = characters at the start of a format mean?
They set byte order (endianness) and alignment. '<' is little-endian, '>' is big-endian, '!' is network byte order (same as big-endian), and '=' is native order with no padding. Always specify one explicitly when reading or writing a file format so your code behaves identically on every machine.
What are the format characters like i, h, f, and s?
Each letter maps to a C type: 'h' is a 2-byte short, 'i' is a 4-byte int, 'f' is a 4-byte float, 'd' is an 8-byte double, and 's' is a bytes string (prefixed with a count, e.g. '4s'). A leading number repeats a code, so '3i' means three ints. struct.calcsize tells you how many bytes a format takes.
Why do I get bytes back from unpack instead of a string?
struct works at the byte level, so the 's' format gives you a bytes object, not a str. Decode it yourself with .decode('utf-8') (or 'ascii') when you know the text encoding. This explicit step keeps you in control of encoding, which matters a lot when parsing real binary formats.