String#bytes
Basic Usage
bytes returns an array of integers, one per byte in the string:
"hello".bytes
# => [104, 101, 108, 108, 111]
Each integer is a value between 0 and 255. No arguments, no block — just the array.
Signature
The method takes no arguments and returns a flat array of integers between 0 and 255. Since the return value is always an Array, you can chain it with other array methods like sum, include?, or each right on the result.
str.bytes -> array
The receiver is any String object. The method takes no arguments and returns an Array of integers, one per byte.
Multi-byte characters are not single bytes
This is where bytes trips up many developers. A UTF-8 character can take 1 to 4 bytes in UTF-8, so the raw byte count and the character count often differ. The string "café" has 4 characters but 5 bytes:
"café".bytes
# => [99, 97, 102, 195, 169]
"café".chars.count # => 4
"café".bytes.count # => 5
The é (U+00E9) encodes as two bytes: 195 and 169. If you loop over bytes expecting one element per character, you will get surprises.
Multi-byte example with CJK characters
For Japanese characters, each kanji in UTF-8 occupies 3 bytes. That means a string of 3 characters expands to 9 bytes, showing how the gap between character count and byte count grows quickly with non-Latin scripts:
s = "日本語"
s.bytes
# => [230, 151, 165, 230, 152, 165, 230, 156, 165]
s.chars.count # => 3
s.bytes.count # => 9
Encoding changes the byte values
The same character sequence produces different bytes depending on the encoding. UTF-8 uses a variable-width scheme where ASCII characters stay at 1 byte and non-ASCII characters expand. Latin-1 and other single-byte encodings keep every character to exactly 1 byte.
"é".encode("UTF-8").bytes
# => [195, 169]
"é".encode("ISO-8859-1").bytes
# => [233]
UTF-8 uses multiple bytes for non-ASCII characters. ISO-8859-1 (Latin-1) uses one byte per character. Always check str.encoding before interpreting byte values.
bytes vs codepoints vs chars
These three methods get confused constantly because they all return array-like results from a string, but each answers a different question about the underlying data.
"café".chars # => ["c", "a", "f", "é"]
"café".codepoints # => [99, 97, 102, 233]
"café".bytes # => [99, 97, 102, 195, 169]
chars returns an array of single-character strings, one per visible character. codepoints gives you Unicode integer values, also one per character — é is codepoint 233 regardless of encoding. bytes gives you the raw byte values, where non-ASCII characters like é span multiple elements (195 and 169 in UTF-8). The distinction matters whenever you are counting or slicing by position rather than iterating over the full array.
Iterating with each_byte
If you need to process each byte individually instead of getting the full array at once, each_byte is an explicit iterator that yields one integer at a time:
byte_values = []
"hello".each_byte { |b| byte_values << b }
byte_values
# => [104, 101, 108, 108, 111]
Both each_byte and bytes without a block return the same values. Using each_byte makes the iteration intent clear in your code because the block signals that you are processing one value at a time rather than building a collection upfront.
Checking bytesize separately
If you only need the count of bytes, bytesize (or bytesize? in newer Ruby) is more direct:
"café".bytesize
# => 5
"hello".bytesize
# => 5
Calling bytes.count gives the same number but constructs an intermediate array before counting it, which costs memory for large strings. For a quick byte count without the allocation overhead, reach for bytesize instead.
Working with binary data
bytes is useful when you need to inspect or manipulate binary data stored in a string:
data = "\xFF\x00\x80"
data.bytes
# => [255, 0, 128]
Once you have the byte array, you can check for specific byte values or patterns with any Array method. For example, include? tells you whether a particular byte appears anywhere in the data, which is useful for detecting magic numbers or delimiters in binary file headers.
# Check if a specific byte value is present
data.bytes.include?(255)
# => true
Ruby strings can hold arbitrary binary data when marked with the BINARY encoding, also known as ASCII-8BIT. This encoding tells the VM to treat every byte as a standalone value rather than interpreting multi-byte sequences as characters. Calling .b on any string returns a binary-encoded copy:
binary_string = "hello".b
binary_string.encoding
# => #<Encoding:ASCII-8BIT>
binary_string.bytes
# => [104, 101, 108, 108, 111]
When you call .b on a string, Ruby marks it as ASCII-8BIT without changing the underlying bytes. This encoding tells Ruby to treat each byte independently rather than interpreting multi-byte sequences. The bytes method then returns exactly the raw values you expect.
Practical example: checksum calculation
A common use case is computing a simple byte-sum checksum:
def byte_sum(str)
str.bytes.sum
end
byte_sum("hello")
# => 532 (104 + 101 + 108 + 108 + 111)
A byte-sum checksum is fast and easy to implement, but it only catches single-bit errors. For stronger integrity guarantees in production code, use a cryptographic hash like SHA-256 instead.
See Also
- String#chars — array of character strings
- String#bytesize — number of bytes without creating an array
- String#encode — convert between encodings
- String#each_char — iterate over characters
- String#slice — extract substrings by index or regex