luaguides

string.char

string.char (...)

Synopsis

string.char (···)

string.char takes zero or more integers and returns a string whose bytes are those integers in order. The result has exactly one byte per argument, so #string.char(a, b, c) == 3. With no arguments it returns the empty string "". It is the standard inverse of string.byte: round-tripping a sequence through byte and then char gives you back the same string.

If you are coming from C, the mental model is “build a char[] from byte values.” If you are coming from Python, the closest equivalent is bytes([...]), with the same caveat that string.char is byte-oriented, not Unicode-aware.

Quick look

print(string.char(72, 101, 108, 108, 111))   --> Hello
print(string.char(65, 66, 67))              --> ABC
print(#string.char())                       --> 0

The first call produces the word Hello from five byte codes. The second packs A, B, C into a single string. The third confirms that an empty call yields the empty string of length 0.

Parameters

string.char is variadic and the manual does not name the arguments. Treat them as a positional list of integer codes.

#NameTypeDefaultDescription
1..N(positional)integernoneInternal numeric code of one byte. Must lie in [0, 255]. The byte appears at the same position in the result string. Floats with an integral value (for example 65.0) are coerced to the integer 65; floats with a non-integral value are rejected.

The valid range is [0, 255] on every 8-bit-clean Lua implementation, including the reference PUC-Rio build, LuaJIT, and Luau. The Lua 5.4 manual still warns that “numeric codes are not necessarily portable across platforms” — defensible advice for code that has to run on exotic or non-standard hosts, but in practice the [0, 255] range has held since Lua 5.0.

Return Value

string.char returns a string whose length equals select('#', ...): each argument produces exactly one byte, in argument order. The output is a raw byte sequence, not characters in any particular encoding — string.char does not decode or interpret the bytes, it just packs them. Lua strings are immutable, so the call always returns a fresh string; there is no in-place variant.

The length claim is the one most worth checking in your own code, because it is what trips up “is the string empty?” checks downstream:

print(#string.char())             --> 0
print(#string.char(72, 105))      --> 2
print(#string.char(65, 66, 67))   --> 3

Examples

Building a known string from byte codes

The most common use is reconstructing an ASCII string from a known sequence of codes. The codes 65, 66, and 67 happen to be uppercase A, B, and C in ASCII, which makes them a friendly example. Once you know the codes, the construction is mechanical: pass them in order and string.char returns the assembled string, with one byte per argument. The length operator # then returns the argument count, which is also the byte count of the result.

print(string.char(65, 66, 67))   --> ABC
print(#string.char(65, 66, 67))  --> 3

The first call packs bytes 65, 66, 67 (the codes for A, B, C) into a string and prints it. The second confirms the length is 3, one byte per argument.

Round-tripping with string.byte

string.char and string.byte are inverses. This is useful for sanity-checking byte sequences coming back from a parser or a binary protocol.

local s = string.char(72, 101, 108, 108, 111)  -- "Hello"
print(s)                          --> Hello
print(string.byte(s, 1, #s))      --> 72  101 108 108 111

The middle line is the human-readable string. The last line reads every byte back out as its numeric code. If you ever see the codes come back in a different order, something in the protocol layer is swapping endianness or dropping bytes.

Empty call and embedded NUL

Two edges worth pinning down: the no-argument call, and the fact that zero bytes are valid bytes.

print(#string.char())                  --> 0
print(#string.char(72, 0, 105))        --> 3
print(string.format("%q", string.char(72, 0, 105)))
--> "H\0i"

string.char() with no arguments is the empty string. string.char(72, 0, 105) is the three-byte sequence H, NUL, i — length 3, not 1. A bare print on a string containing NUL just stops at the NUL when writing to a C stream, so string.format("%q", ...) is the cleanest way to see the full content in the REPL. Lua strings are not null-terminated, so this matters in practice any time you handle binary data; see the binary data guide for a deeper look.

Out-of-range input

Passing a value outside [0, 255] is a hard error.

print(pcall(string.char, -1))   --> false   value out of range
print(pcall(string.char, 256))  --> false   value out of range
print(pcall(string.char, 65.5)) --> false   number has no integer representation

Wrap untrusted input in pcall if you do not control the source. The error message comes from luaL_argcheck in lstrlib.c, so the exact text is "value out of range" for integer out-of-range, and the standard coercion error for non-integral floats.

Common Pitfalls

string.char is small and well-specified, but the asymmetries with related functions catch people out. The cases below are the ones worth a defensive comment in your own code.

  1. string.char is a static library function. The expression "A":char(65) is a runtime error because there is no string:char method. By contrast, string.byte does have a method form (s:byte(i, j)), and the asymmetry catches people regularly.

  2. string.char only knows about 8-bit bytes. It will not turn a Unicode codepoint into the right UTF-8 for you. For codepoint-level work in Lua 5.3+, prefer utf8.char from the utf8 standard library, which expects a single codepoint and returns the proper UTF-8 encoding. The two snippets below produce the same three-byte UTF-8 sequence for U+2603 (the snowman character):

    -- Hand-built UTF-8 bytes for U+2603 (snowman) using string.char
    print(string.format("%q", string.char(0xE2, 0x98, 0x83)))
    --> "\226\152\131"
    
    -- Same codepoint via the stdlib utf8 helper
    print(string.format("%q", utf8.char(0x2603)))
    --> "\226\152\131"

    utf8.char is less error-prone on supplementary planes; string.char is fine when you already have bytes in hand.

  3. Values outside [0, 255] are an error rather than a silent clamp. If you are reading bytes off the wire and one happens to be 256 because of a parser bug, you want to hear about it, not silently get a truncated string.

  4. Float handling is half-strict. string.char(65.0) returns "A" because Lua 5.4 coerces integral floats, but string.char(65.5) errors with “number has no integer representation.” The coercion is automatic, not optional:

    print(string.char(65.0))              --> A
    print(pcall(string.char, 65.5))       --> false   number has no integer representation
  5. string.format("%c", n) is not a drop-in replacement for string.char. It also produces "A" from 65, but it only handles a single character per call and is noticeably slower for building byte sequences. Use string.char for the multi-byte case:

    -- Slow: one format call per byte, multiple intermediate strings
    local slow = string.format("%c%c%c", 72, 105, 33)  -- "Hi!"
    -- Fast: one char call, one allocation
    local fast = string.char(72, 105, 33)              -- "Hi!"
  6. Very large argument lists allocate one big string. Nothing in the spec caps the argument count, but string.char builds a single contiguous string, so feeding it millions of bytes at once can pressure the allocator. Build in chunks if you have a hard upper bound on the result size.

See Also

  • string.byte — the direct inverse. Reads bytes out of a string as integers.
  • Lua strings and patterns — the beginner-friendly introduction to the byte-vs-character model that this function relies on.
  • Binary data in Luastring.char is the standard tool for building byte blobs; this guide shows the full pattern for reading and writing binary records.
  • String patterns — patterns operate on raw strings, the same level string.char produces.