luaguides

string.byte

string.byte(s [, i [, j]])

string.byte returns the numeric codes of one or more bytes from a string. It is the byte-level inspection function in Lua’s standard string library, and it pairs naturally with string.char, which is its inverse.

In Lua 5.3 and later, strings are explicitly sequences of bytes, and string.byte walks them by byte. For non-ASCII text that contains UTF-8, you usually want utf8.codepoint from the standard utf8 library instead, but for binary data and ASCII processing string.byte is the right tool.

Synopsis

The function form and the method form are exactly equivalent — pick whichever reads better at the call site.

-- Function form
string.byte(s [, i [, j]])

-- Method form
s:byte([i [, j]])

Parameters

string.byte takes one required string and up to two integer indices.

ParameterTypeDefaultDescription
sstringrequiredThe string to inspect. Must be a string; passing any other type raises an error.
iinteger1Start position. 1-based, like all Lua string indices. Negative values count back from the end: -1 is the last byte, -2 is the second-last, and so on.
jintegeriEnd position, inclusive. Same 1-based and negative-index rules as i. Defaults to i (not to 1), so a two-argument call returns a single byte.

Both indices go through the usual Lua string-index correction: i and j are clamped to lie within [1, #s], and j is additionally capped at #s. Float arguments that have an exact integer value are accepted; non-integral floats raise an integer-conversion error in Lua 5.3+.

Return Values

string.byte returns one integer per byte in the inclusive range [i, j], where each integer is the internal numeric code of that byte. On a standard Lua 5.4 build those codes are the unsigned 8-bit values 0..255.

It returns no values (not nil, just nothing) when any of these hold after index correction:

  • i > j — the range is empty.
  • the corrected i is past the end of the string.
  • the string itself is empty.

A range of N bytes produces N separate return values, not a table. The difference between capturing one byte and capturing all of them is just how you write the call:

local first = string.byte("ABC", 1, 3)     -- 65 (the rest are discarded)
local all   = {string.byte("ABC", 1, 3)}   -- {65, 66, 67}
local a, b, c = string.byte("ABC", 1, 3)   -- 65 66 67

If you assign the result to a single variable, only the first byte is kept and the rest are discarded. Use a table constructor or select to capture all of them.

Examples

Single byte and small ranges

print(string.byte("ABCDE"))         -- 65            ('A')
print(string.byte("ABCDE", 1))      -- 65            (explicit i = 1)
print(string.byte("ABCDE", 2))      -- 66            ('B')
print(string.byte("ABCDE", 3, 4))   -- 67  68        ('C', 'D')
print(string.byte("ABCDE", -1))     -- 69            ('E', last byte)
print(string.byte("ABCDE", -3, -1)) -- 67  68  69    (last three bytes)

Notice the two-argument form: string.byte("ABCDE", 2) returns one byte, not the first two. That is because j defaults to i, not to 1.

Walking a string byte by byte

The canonical way to iterate over the bytes of a string. s:byte(i) reads the i-th byte, and string.char round-trips it back into a one-character string.

local s = "Lua"
for i = 1, #s do
    print(i, s:byte(i), string.char(s:byte(i)))
end
-- 1   76  L
-- 2  111  u
-- 3   97  a

Building a byte array

Collecting every byte into a table is useful before searching for runs, hashing, or feeding the values into string.char to build a new buffer.

local function bytes(s)
    local out = {}
    for i = 1, #s do
        out[i] = s:byte(i)
    end
    return out
end

local t = bytes("hi")
print(#t)            -- 2
print(t[1], t[2])    -- 104  105

Out-of-range and edge cases

These show the silent behavior described in the return-values section. None of them raise an error — string.byte simply returns fewer values than you might expect, so callers that assume one byte came back will read nil from the first return and may crash later. Guard with if b then ... end or check #s first.

print(string.byte("AB", 1, 100))   -- 65  66       (j clamped to #s)
print(string.byte("AB", 3))        -- (nothing)    (i past end)
print(string.byte("AB", 3, 2))     -- (nothing)    (i > j)
print(string.byte("", 1))          -- (nothing)    (empty string)
print(string.byte("A", 1.0))       -- 65           (integral float accepted)

8-bit-clean data

Because Lua 5.4 strings are sequences of bytes, string.byte handles embedded NULs and full 8-bit values without surprise. Every byte from 0 to 255 is a valid result, which makes the function the natural building block for parsing binary protocols, reading image headers, or decoding custom file formats in pure Lua.

local buf = "\0\127\255"
for i = 1, #buf do
    print(i, buf:byte(i))
end
-- 1   0
-- 2  127
-- 3  255

This is why string.byte is the foundation for parsing binary protocols and file formats in Lua — see the working with binary data guide for a longer treatment.

Common Pitfalls

  • j defaults to i, not to 1. A two-argument call like string.byte(s, 5) returns the 5th byte, not bytes 1 through 5.
  • No zero index. string.byte(s, 0) returns nothing. Lua strings are 1-indexed; writing 0 is a frequent off-by-one bug.
  • Out-of-range i is silent. No error and no nil — you simply get zero return values. Code that assumes one byte came back will then read nil from the first return and may crash later. Guard with if b then ... end or check #s first.
  • j past the end is clamped, i past the end is not. The function widens the window from a valid start, but if i is already beyond #s, it returns nothing instead of reaching back.
  • Bytes, not characters. For UTF-8 input, a non-ASCII code point occupies multiple bytes and string.byte returns the individual byte values, not a code point. Use utf8.codepoint from the utf8 standard library when you need Unicode-aware handling.
  • It is a vararg-returning function. Assigning the result to a single variable keeps only the first byte. To capture a range, use {string.byte(s, 1, 3)} or expand the call into multiple targets.
  • Float indices that are not integers raise an error in Lua 5.3+. Pass integers (or floats with an exact integer value).

See Also

  • string.find — returns byte offsets that can be fed straight back into string.byte to inspect what was matched.
  • string.gsub — when used with a function replacement, frequently calls string.byte to inspect or rewrite individual bytes.
  • Working with binary data in Lua — a broader guide that uses string.byte as its main tool for parsing buffers.
  • Strings and patterns tutorial — the beginner-friendly walkthrough of the string library, including the Unicode-aware utf8 module.