luaguides

string.format()

string.format(fstr, ...)

Overview

string.format is Lua’s printf. It takes a format string containing literal text mixed with %-directives, then substitutes each directive with a value from the variadic arguments. The result is a new string; nothing is mutated.

You can also call it as a method: ("pi ≈ %.3f"):format(math.pi) works because the string library installs a metatable on the string type. Both forms do the same thing under the hood.

It pairs with tostring: tostring turns a value into a string with no control over the shape, string.format lets you pick the shape exactly.

Signature

string.format(fstr: string, ...: any) -> string

The first argument is the format string. Every additional argument is consumed by a %-directive in order. If there are more directives than values, string.format raises an error. The return is always a single string.

Parameters

#NameTypeDescription
1fstrstringFormat string. Literal text mixed with %-directives. Use %% to emit a literal percent sign.
2…N...anyOne value per %-directive. The type must match the specifier: %d wants a number, %s wants something with a __tostring metamethod, and so on. Mismatches raise an error.

string.format does not mutate fstr or any of the arguments. If you need to reuse the format string, store it in a variable and pass it in. Strings in Lua are immutable, so this is rarely a concern, but it is worth knowing.

Conversion Specifiers

A specifier is the single letter at the end of a directive, like d in %d or s in %10s.

SpecMeaningAccepts
d / iSigned decimal integerinteger or float (truncated toward zero)
oOctal integerinteger or float
uDecimal integer, negatives wrapped to 2^64integer or float
x / XHex, lowercase / uppercaseinteger or float
fFixed-point float, default precision 6number
e / EScientific notationnumber
g / GShortest of %f or %e, trailing zeros strippednumber
a / AHex float, lowercase / uppercase (Lua 5.4+)float
cSingle character (byte value)integer
sStringstring (or any value with __tostring)
qLua-source-quoted stringstring
%%Literal %

A few of the most common specifiers in action:

-- Integer in three bases
print(string.format("%d / %x / %o", 255, 255, 255))  -- "255 / ff / 377"

-- Float with controlled precision, plus the 5.4 hex-float form
print(string.format("%.3f | %a", math.pi, math.pi))  -- "3.142 | 0x1.921p+1"

-- String padding (left vs right) and truncation by precision
print(string.format("[%-8s][%8s][%8.3s]", "hi", "hi", "hello"))
-- "[hi      ][      hi][     hel]"

The q specifier is the one most people miss. It escapes a string so it can be re-read as a Lua literal, useful for serialization that will be load-ed back:

local user = 'a "quoted" string\nwith newline'
print(string.format("%q", user))
-- "a \"quoted\" string\nwith newline"

Hex floats (%a, %A) are a 5.4 addition. They do not exist in 5.1/5.2/5.3. If you need to support older versions, branch on _VERSION or feature-detect at startup.

Flags, width, precision

A full directive has the form %[flags][width][.precision]specifier.

Flags:

  • - — left-justify within the field. Default is right-justified.
  • + — always show the sign, even for positive numbers.
  • (space) — leading space on positives, minus on negatives. Ignored if + is also set.
  • 0 — zero-pad on the left, after the sign.
  • # — alternate form. o prefixes 0; x/X prefix 0x/0X; a/A/e/E/f keep a decimal point even with no digits after; g/G keep trailing zeros.

Width is the minimum field width. Precision is decimal digits for floats, or max string length for %s.

print(string.format("%-10s|%10s|", "left", "right"))
-- left      |     right|

print(string.format("%05d", 42))      -- "00042"
print(string.format("%+05d", 42))     -- "+0042"

There is no * placeholder like C’s printf. If you need variable width or precision at runtime, build the format string yourself rather than hard-coding the digits:

local width, prec = 12, 4
local fmt = "%" .. width .. "." .. prec .. "f"
print(string.format(fmt, math.pi))
--      3.1416

Width and precision are each limited to two digits, so 99 is the largest value you can hard-code in a directive. Going higher raises “invalid conversion specification”. Build the format string dynamically for anything bigger.

Examples

Aligned table output

This is the bread-and-butter use case. string.format lets you build fixed-width columns without any external library:

for i, name in ipairs({"Alice", "Bob", "Charlie"}) do
  print(string.format("%2d. %-10s (score: %5.1f)",
    i, name, math.random() * 100))
end
--  1. Alice      (score:  73.4)
--  2. Bob        (score:  12.8)
--  3. Charlie    (score:  99.0)

%-10s left-justifies the name in a 10-wide field. %5.1f right-justifies the score with one digit of precision. The 2d keeps single-digit numbers aligned with double-digit ones by padding the leading space.

Safe string embedding with %q

Use %q when building Lua source from data. It is the right tool for serialization that needs to round-trip:

local function quote(s) return string.format("%q", s) end

local payload = "line1\nline2\ttab"
local chunk = "return { msg = " .. quote(payload) .. " }"
print(chunk)
-- return { msg = "line1\nline2\ttab" }

The result is valid Lua source: load(chunk)() reconstructs the original string byte for byte, control characters and all. The escaped form is safe to concatenate into Lua chunks without worrying about unescaped quotes or stray newlines.

Hex float (Lua 5.4+)

print(string.format("%a", 1.5))         -- "0x1.8p+0"
print(string.format("%.3a", math.pi))  -- "0x1.921p+1"

Useful for debugging floating-point representation without pulling in a third-party library. If you need to compare a value’s exact bit pattern, this is the cleanest way to print it.

Method-call form

print(("pi ≈ %.3f"):format(math.pi))
-- pi ≈ 3.142

This works because the string library sets a metatable on the string type with __index pointing to the string table. It is syntactic sugar: same call, same result. Pick whichever reads better in context.

Common Pitfalls

Most of these show up in code review, not in the manual. Three of the most common ones are illustrated below; the rest are listed in the numbered summary that follows.

-- Pitfall 1: width or precision over 99 errors out.
-- string.format("%100d", 1)  -- error: invalid conversion specification
string.format("%" .. 100 .. "d", 1)  -- works: 99 spaces + "1"

-- Pitfall 3: booleans are rejected by every specifier.
-- string.format("%s", true)  -- error: bad argument
string.format("%s", tostring(true))  -- "true"

-- Pitfall 4: nil raises an error rather than printing "nil".
local value = nil
string.format("%s", value or "(missing)")  -- "(missing)"
  1. Width or precision over 99 errors out. The parser only reads two digits. Build the format string dynamically for anything bigger.
  2. * is not supported. No printf("%*d", width, n) in Lua. Same workaround: build the format string.
  3. Booleans are rejected by every specifier. string.format("%s", true) errors. Use tostring(true) first, or convert to a string up front.
  4. nil raises an error rather than printing "nil". Guard missing values explicitly before passing them in.
  5. No locale awareness. No thousand separators, no comma decimal. Output is locale-independent and machine-independent, which is usually what you want.
  6. %u is misleading. It is not a true unsigned type. It just wraps negatives to 2^64 + n, so -1 prints as 18446744073709551615.
  7. Floats with huge magnitude to %d/%x error when out of integer range. string.format("%d", 1e30) fails. Convert with math.floor first, or use %f/%g for large values.
  8. %c only accepts an integer — a float with a fractional part errors. Cast with math.floor or convert explicitly.
  9. %% is required for a literal %. A bare % that is not part of a valid directive raises “invalid conversion specification”. This includes typos like %-s.
  10. %q does not accept non-strings in 5.4. Pass a string, or wrap with tostring first.
  11. Rounding is platform-dependent for %g. On most C libraries, half-to-even (banker’s rounding) is used. Watch out for financial code where rounding mode matters.
  12. Hex floats are 5.4+ only. %a and %A do not exist in 5.1/5.2/5.3. Code that needs to run on LuaJIT (which is closer to 5.2) cannot use them.

See Also