String#unpack
str.unpack(template) -> array | str.unpack1(template) -> value The unpack method decodes a binary string (such as one created by pack) according to a template format string. It returns an array of values extracted from the binary data. The unpack1 method works identically but returns only the first value instead of an array—useful when you know the template will produce exactly one result.
Binary code is easier to read when the template is treated like documentation for the byte layout. A short directive can describe how many bytes are stored, whether the number is signed, and how the text should be decoded. That makes unpack a strong fit for file headers, network packets, and other compact formats.
The key idea is that the template tells the story of the data in the same place you read it. That keeps byte parsing from turning into a pile of magic numbers, and it gives you a stable place to update when the structure changes.
Syntax
str.unpack(template)
str.unpack1(template)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
template | String | — | A format string specifying how to decode the binary data. Each character represents a directive for decoding. |
Common template directives
| Directive | Meaning |
|---|---|
C | Unsigned 8-bit integer |
S | Unsigned 16-bit integer (little-endian) |
L | Unsigned 32-bit integer (little-endian) |
s | Signed 16-bit integer |
l | Signed 32-bit integer |
f | Float (single precision) |
d | Double precision float |
A | ASCII string (trailing nulls and spaces stripped) |
a | ASCII string (preserves trailing characters) |
H | Hex string (high nibble first) |
h | Hex string (low nibble first) |
B | Bit string (most significant bit first) |
b | Bit string (least significant bit first) |
x | Null byte |
@ | Null fill to absolute position |
Prefixes: < = little-endian, > = big-endian, N = network byte order (big-endian).
Those directives are enough for most small binary formats. Once you know which byte order the data uses, the rest of the template is usually just a matter of matching the field sizes to the protocol or file layout.
Examples
Basic integer unpacking
# Pack integers into binary, then unpack
binary = [255, 0, 16, 0].pack('C*')
# => "\xFF\x00\x10\x00"
binary.unpack('C*')
# => [255, 0, 16, 0]
binary.unpack1('C')
# => 255
The simplest use of unpack is reading a stream of values in a single pass. When the data has structure beyond a flat list of numbers, the template can include multiple directives that name each field by its position and type. The example below reads a 4-byte network header with a version byte, a type byte, and a 2-byte length field, using n for network byte order to match the protocol specification directly in the template string.
Unpacking a network packet header
# Simulate a 4-byte header: version(1 byte) + type(1 byte) + length(2 bytes)
packet = "\x04\x01\x00\x50" # version=4, type=1, length=80
packet.unpack('CCn')
# => [4, 1, 80]
# Using unpack1 for single values
packet.unpack1('C')
# => 4
# Big-endian 16-bit integer
packet.unpack('C2n')
# => [4, 1, 80]
The repeated examples show that the same bytes can be expressed in more than one way. That is useful when you are trying to choose between a compact template and one that reads more naturally to the next person.
Once you understand one packet shape, you can usually adapt the template by changing only the directives that describe the field sizes. That keeps the decoding logic close to the binary layout instead of scattering magic numbers through the parser.
Working with binary structs
# Unpack a timestamp (seconds + milliseconds)
binary = [1700000000, 500].pack('L L')
# => ">\xF5F\x00\x00\x01\xf4"
binary.unpack('L2')
# => [1700000000, 500]
# Using unpack1 for the first component
binary.unpack1('L')
# => 1700000000
The unpack1 form is convenient when the template only produces one value and you do not want to unwrap an array afterward. It keeps the call site lean while still using the same template language as unpack. This is especially helpful when parsing binary headers where each field is read one at a time in sequence, keeping the extraction code short and the field order visible at a glance.
Hex string decoding
hex_string = "48656c6c6f" # "Hello" in hex
hex_string.unpack('H*')
# => ["48656c6c6f"]
# Decode pairs of hex digits
hex_string.unpack('H2' * 5)
# => ["48", "65", "6c", "6c", "6f"]
hex_string.pack('H*').unpack('C*')
# => [72, 101, 108, 108, 111]
Working from encoded hex strings back into bytes is a good reminder that pack and unpack are two sides of the same idea. One prepares data for storage or transport, and the other turns it back into values you can work with directly.
Common Patterns
Network protocol parsing: Unpack binary protocols byte-by-byte using C directives.
Binary file reading: Read file contents and unpack structured data like headers.
Data serialization: Use pack/unpack for efficient binary serialization without overhead of JSON/YAML.
Bit-level manipulation: Extract individual bits with B or b directives.
When you are dealing with raw bytes, the main thing to remember is that the template is the contract. If the data layout changes, the template has to change too. Keeping that logic in one place makes the rest of the parser much easier to follow.