Skip to Content
Course content

237: The struct Module for Binary Data

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I recently ran into a situation where I had to interface with a piece of legacy hardware that sends data over a socket. It doesn't send JSON or XML; it sends a raw stream of bytes. If you've only ever worked with high-level APIs, this feels like staring at a wall of gibberish. Let's walk through how I tackled this using the struct module.

Dealing with a mysterious byte string

Suppose my hardware device sends a "Sensor Packet." According to the manual, the packet is exactly 7 bytes: one byte for the Sensor ID, four bytes for a floating-point temperature, and two bytes for a status code. I captured a packet, and it looks like this in Python:

data = b'\x01\x00\x00\x80\x3f\x05\x00'

My first instinct—before I remembered the struct module—was to try and slice this manually. I know the first byte is the ID, so that's easy:

sensor_id = data[0]
print(f"Sensor ID: {sensor_id}") # This works, it's 1.

But then I hit the temperature. It's 4 bytes starting at index 1. I tried using int.from_bytes(), but that only works for integers. The temperature is a 32-bit float (IEEE 754). Trying to manually calculate a float from bytes is a rabbit hole I never want to go down again. That's where struct comes in.

Letting struct do the heavy lifting

The struct module allows us to "unpack" these bytes into Python types using a format string. I'll try to unpack the whole thing at once. I'll use B for an unsigned char (1 byte), f for a float (4 bytes), and H for an unsigned short (2 bytes).

import struct

# I'll try this format string: B (unsigned char), f (float), H (unsigned short)
result = struct.unpack('BfH', data)
print(result)

When I ran this, I got a struct.error: unpack requires a buffer of 8 bytes. Wait, why 8? I only have 7 bytes. This is a classic struct gotcha. By default, Python uses "native" alignment, which means it pads the data to align with the computer's architecture (usually 4 or 8 bytes). It's trying to put that float on a 4-byte boundary, which adds an invisible padding byte after the first B.

To fix this, I need to tell Python to ignore alignment and use "standard" sizes. I do this by prefixing the format string with < (for little-endian) or > (for big-endian). Most modern hardware uses little-endian.

# Adding '<' tells Python: "no padding, little-endian"
result = struct.unpack('<BfH', data)
print(result) 
# Output: (1, 1.0, 5)

There it is. Sensor 1, 1.0 degrees, status code 5. Much cleaner than doing bit-shifts manually.

Packing it back up for the wire

Now, what if I need to send a command back to the device? I can't just send a Python tuple; I need to convert those values back into a byte string. This is where struct.pack comes in. It's the exact inverse of unpack.

Let's say I want to send a status update: Sensor 2, temperature 25.5, status 10.

# Using the same format string as before
packet = struct.pack('<BfH', 2, 25.5, 10)
print(packet)
# Output: b'\x02\x00\x00\xcc\x41\x0a\x00'

If you look at that output, it looks like a mess again, but the hardware will see exactly what it expects: a 1-byte integer, a 4-byte float, and a 2-byte integer. I've effectively created a binary protocol without having to worry about the underlying binary representation of floating-point numbers.

Choosing the right characters

You'll need to keep the format character table handy when using this module. Here are the ones I use most often:

  • b: Signed char (1 byte)
  • B: Unsigned char (1 byte)
  • h: Signed short (2 bytes)
  • H: Unsigned short (2 bytes)
  • i: Signed int (4 bytes)
  • I: Unsigned int (4 bytes)
  • f: Float (4 bytes)
  • d: Double (8 bytes)

Just remember: always start your string with < or >. If you don't, your code might work on your machine but crash on another because of how different CPUs handle memory alignment.




📋 Practical Task

Exercise: Weather Station Telemetry Parser

You are writing a driver for a weather station that sends a binary packet containing the following data in little-endian format:

  • Station ID: Unsigned Short (2 bytes)
  • Temperature: Float (4 bytes)
  • Humidity: Unsigned Char (1 byte)
  • Pressure: Unsigned Int (4 bytes)

Your Task:

  1. Create a function parse_weather_packet(data) that takes a bytes object and returns a dictionary with the keys "id", "temp", "humidity", and "pressure".
  2. Test your function using this sample packet: b'\x0a\x00\x00\x00\x20\x41\x32\x10\x27\x01\x00'.
  3. Verify that the parsed values are: ID=10, Temp=10.0, Humidity=50, Pressure=4631.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.