Skip to content

struct Module Complexity

The struct module handles binary data conversions, packing Python values into bytes and unpacking bytes into Python values using format strings.

Complexity Reference

Operation Time Space Notes
Struct() compilation O(m) O(m) m = format string length; compile once
pack() O(k + B) O(B) k = fields, B = total byte width. Output is B bytes however few fields produce it. The module caches compiled formats, so a repeated format is not re-parsed
unpack() O(k + B) O(k + B) The tuple holds k elements, but byte-string fields carry their bytes with them
pack_into() O(k + B) O(1) Writes into a caller-supplied buffer, so no output is allocated
unpack_from() O(k + B) O(k + B) Same result space as unpack()
calcsize() O(m) O(1) m = format string length

Format Strings

Character Types

import struct

# Format string characters
# '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)
# 'l' = signed long      (4 bytes)
# 'L' = unsigned long    (4 bytes)
# 'q' = signed long long (8 bytes)
# 'Q' = unsigned long long (8 bytes)
# 'f' = float            (4 bytes)
# 'd' = double           (8 bytes)
# 's' = char[]           (variable)
# 'p' = pascal string    (variable)
# 'P' = void*            (pointer)

# Calculate size - O(m) where m = format length
size = struct.calcsize('i')      # 4 bytes
size = struct.calcsize('ihh')    # 8 bytes (4+2+2)
size = struct.calcsize('ihhf')   # 12 bytes (4+2+2+4)

Byte Order and Alignment

import struct

# Byte order prefix (optional, default='@' native)
# '@' = native (default)
# '=' = native (no alignment)
# '<' = little-endian
# '>' = big-endian
# '!' = network (big-endian)

# Native order - O(m) parse
native = struct.calcsize('i')       # 4

# Little-endian - O(m) parse
little = struct.calcsize('<i')      # 4

# Big-endian - O(m) parse
big = struct.calcsize('>i')         # 4

# With alignment - O(m) parse
aligned = struct.calcsize('@ii')    # 8 (with padding)
unaligned = struct.calcsize('=ii')  # 8 (no padding)

Packing Data

Simple Packing

import struct

# Pack single value - O(1)
# Format: integer (4 bytes)
bytes_data = struct.pack('i', 42)
print(bytes_data)  # b'*\x00\x00\x00' (little-endian)

# Pack multiple values - O(n) for n values
bytes_data = struct.pack('ihh', 100, 200, 300)
# 4 bytes (int) + 2 bytes (short) + 2 bytes (short) = 8 bytes

# Pack with byte order - O(n)
bytes_data = struct.pack('>i', 42)    # Big-endian
bytes_data = struct.pack('<i', 42)    # Little-endian

String Packing

import struct

# Pack fixed-length string - O(n)
text = "Hello"
bytes_data = struct.pack('5s', text.encode())  # 5-byte string

# Pack with padding
name = "Bob"
bytes_data = struct.pack('10s', name.encode())  # Padded to 10 bytes

# Pack multiple strings
bytes_data = struct.pack('5s3s', b"Hello", b"Bob")  # 8 bytes total

Pack Into Buffer

import struct

# Pack into existing buffer - O(n) for n fields
buffer = bytearray(20)

# Write at offset 0
struct.pack_into('i', buffer, 0, 42)

# Write at offset 4
struct.pack_into('h', buffer, 4, 100)

# Write at offset 6
struct.pack_into('f', buffer, 6, 3.14)

print(buffer[:10])  # First 10 bytes with packed data

Unpacking Data

Simple Unpacking

import struct

# Pack first
bytes_data = struct.pack('ihhf', 100, 200, 300, 3.14)

# Unpack all - O(n)
values = struct.unpack('ihhf', bytes_data)
print(values)  # (100, 200, 300, 3.140000104904175)

# Unpack specific subset
values = struct.unpack('ih', bytes_data[:6])  # Skip last values
print(values)  # (100, 200)

Unpack From Buffer

import struct

# Create buffer with packed data
buffer = bytearray(20)
struct.pack_into('i', buffer, 0, 42)
struct.pack_into('h', buffer, 4, 100)
struct.pack_into('f', buffer, 6, 3.14)

# Unpack from buffer - O(n)
value1 = struct.unpack_from('i', buffer, 0)[0]   # 42
value2 = struct.unpack_from('h', buffer, 4)[0]   # 100
value3 = struct.unpack_from('f', buffer, 6)[0]   # 3.14

Struct Objects (Compiled Format)

Create and Reuse Struct

import struct

# Create compiled struct - O(m) once, then O(n) per operation
header_format = struct.Struct('4sI')  # 4-char string + unsigned int

# Pack with compiled struct - O(n) for n fields
bytes_data = header_format.pack(b"HEAD", 12345)

# Unpack with compiled struct - O(n) for n fields
header, version = header_format.unpack(bytes_data)
print(header)    # b'HEAD'
print(version)   # 12345

# Size calculation - O(1)
size = header_format.size  # 8

Struct for Network Protocol

import struct

# Message format: type(1) + length(2) + timestamp(4) + data
class Message:
    HEADER_FORMAT = struct.Struct('!BHI')  # Network byte order

    def __init__(self, msg_type, timestamp, data):
        self.type = msg_type
        self.timestamp = timestamp
        self.data = data

    def serialize(self):
        """Pack to bytes - O(n)"""
        header = self.HEADER_FORMAT.pack(
            self.type,
            len(self.data),
            self.timestamp
        )
        return header + self.data

    @classmethod
    def deserialize(cls, data):
        """Unpack from bytes - O(n)"""
        header_size = cls.HEADER_FORMAT.size
        msg_type, length, timestamp = cls.HEADER_FORMAT.unpack(
            data[:header_size]
        )
        payload = data[header_size:header_size + length]
        return cls(msg_type, timestamp, payload)

# Usage
msg = Message(1, 1234567890, b"Hello")
bytes_msg = msg.serialize()
msg2 = Message.deserialize(bytes_msg)

Common Patterns

Binary File I/O

import struct

class BinaryWriter:
    """Write binary data to file"""

    def __init__(self, filename):
        self.file = open(filename, 'wb')

    def write_int(self, value):
        """Write integer - O(1)"""
        self.file.write(struct.pack('i', value))

    def write_string(self, value, length):
        """Write fixed-length string - O(n)"""
        self.file.write(struct.pack(f'{length}s', value.encode()))

    def close(self):
        self.file.close()

class BinaryReader:
    """Read binary data from file"""

    def __init__(self, filename):
        self.file = open(filename, 'rb')

    def read_int(self):
        """Read integer - O(1)"""
        return struct.unpack('i', self.file.read(4))[0]

    def read_string(self, length):
        """Read fixed-length string - O(n)"""
        return struct.unpack(f'{length}s', self.file.read(length))[0]

    def close(self):
        self.file.close()

# Usage
writer = BinaryWriter("data.bin")
writer.write_int(42)
writer.write_string("Hello", 10)
writer.close()

reader = BinaryReader("data.bin")
value = reader.read_int()
text = reader.read_string(10)
reader.close()

Network Packet Parsing

import struct

class PacketHeader:
    """Parse binary packet header"""

    FORMAT = struct.Struct('!HHBBHH')  # Network byte order
    # Destination port (H)
    # Source port (H)
    # Sequence (B)
    # Flags (B)
    # Window (H)
    # Checksum (H)

    def __init__(self, data):
        """Parse header - O(1)"""
        if len(data) < self.FORMAT.size:
            raise ValueError("Insufficient data")

        self.dst_port, self.src_port, self.seq, self.flags, \
            self.window, self.checksum = self.FORMAT.unpack(
                data[:self.FORMAT.size]
            )

    def to_bytes(self):
        """Serialize header - O(1)"""
        return self.FORMAT.pack(
            self.dst_port, self.src_port, self.seq,
            self.flags, self.window, self.checksum
        )

# Parse packet
packet_data = b'\x00P\x00P\x01\x00\x00\x10\x12\x34'
header = PacketHeader(packet_data)
print(f"Dst: {header.dst_port}, Src: {header.src_port}")

C Structure Mapping

import struct

# Map C struct: typedef struct { int id; float score; } Result;
class CResult:
    """Map to C struct"""

    STRUCT_FORMAT = struct.Struct('if')  # int + float

    def __init__(self, id=0, score=0.0):
        self.id = id
        self.score = score

    def pack(self):
        """Convert to C struct bytes - O(1)"""
        return self.STRUCT_FORMAT.pack(self.id, self.score)

    @classmethod
    def unpack(cls, data):
        """Create from C struct bytes - O(1)"""
        id, score = cls.STRUCT_FORMAT.unpack(data)
        return cls(id, score)

# Usage
result = CResult(42, 95.5)
data = result.pack()
result2 = CResult.unpack(data)

Performance Considerations

Time Complexity

All timings on this page are from CPython 3.11 on one Linux machine and are illustrative, not portable.

  • pack(): O(k + B) for k fields and B bytes. Field count carries the larger constant: '100i' and '400s' both produce 400 bytes, and the hundred-field version measured 5.2x the single-field one - but '40000s' is one field and still costs 9.6x '400s', so B is not negligible
  • unpack(): O(k + B), and the returned tuple holds the bytes too
  • Struct creation: O(m) one-time cost (m = format length)
  • pack_into(): O(k + B) time, O(1) extra space

Space Complexity

  • Packed bytes: O(B) produced by pack(), or none by pack_into()
  • Unpacked tuple: O(k + B) returned by unpack(), since byte-string fields carry their bytes

Optimization Tips

The module-level functions keep a cache of compiled formats, so a format you use repeatedly is parsed once whether or not you pre-compile it. Pre-compiling saves the cache lookup, which is worth about 15%.

import struct

# Fine: the format is parsed once and then found in the cache
for i in range(1000):
    data = struct.pack('i', i)  # 67 ns per call

# Slightly better: no cache lookup either
int_struct = struct.Struct('i')
for i in range(1000):
    data = int_struct.pack(i)  # 58 ns per call

The saving is larger when the format is not in the cache, but only if the Struct is reused. Sweeping 58 distinct formats, with _clearcache() called each time - a forced cold cache, not natural capacity churn:

µs per sweep
Module call, cold cache 46.2
Build a Struct then pack, per format 42.9
Reuse Struct objects built beforehand 14.2

Pre-compiling a format you use once saves nothing: you pay the same parse, just at a different moment. The 3x is for reuse. So build your Struct objects where you build the rest of your constants, and do not bother for a one-shot format.

Format Modifiers

Repetition

import struct

# Pack 10 integers - O(n)
data = struct.pack('10i', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

# Unpack 10 integers - O(n)
values = struct.unpack('10i', data)
print(len(values))  # 10

# With byte order
data = struct.pack('>10H', *range(10))  # 10 big-endian shorts

Error Handling

import struct

# Format must match data size
try:
    struct.unpack('i', b'AB')  # O(m) - size check fails before any unpacking
except struct.error as e:
    print(f"Unpack error: {e}")

# Invalid format character
try:
    struct.pack('z', 42)  # O(m) - rejected while parsing the format
except struct.error as e:
    print(f"Pack error: {e}")
# Both failures cost format parsing only. Note that the module-level
# functions cache compiled formats, so a repeated format is not re-parsed on
# every call - pre-compiling a Struct saves the cache lookup, worth about
# 15% here, and saves the parse itself only when the cache misses