Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
237: The struct Module for Binary Data
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:
- Create a function
parse_weather_packet(data)that takes a bytes object and returns a dictionary with the keys"id","temp","humidity", and"pressure". - Test your function using this sample packet:
b'\x0a\x00\x00\x00\x20\x41\x32\x10\x27\x01\x00'. - Verify that the parsed values are: ID=10, Temp=10.0, Humidity=50, Pressure=4631.
There are no comments for now.