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
| # | Name | Type | Description |
|---|---|---|---|
| 1 | fstr | string | Format string. Literal text mixed with %-directives. Use %% to emit a literal percent sign. |
| 2…N | ... | any | One 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.
| Spec | Meaning | Accepts |
|---|---|---|
d / i | Signed decimal integer | integer or float (truncated toward zero) |
o | Octal integer | integer or float |
u | Decimal integer, negatives wrapped to 2^64 | integer or float |
x / X | Hex, lowercase / uppercase | integer or float |
f | Fixed-point float, default precision 6 | number |
e / E | Scientific notation | number |
g / G | Shortest of %f or %e, trailing zeros stripped | number |
a / A | Hex float, lowercase / uppercase (Lua 5.4+) | float |
c | Single character (byte value) | integer |
s | String | string (or any value with __tostring) |
q | Lua-source-quoted string | string |
%% | 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.oprefixes0;x/Xprefix0x/0X;a/A/e/E/fkeep a decimal point even with no digits after;g/Gkeep 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)"
- Width or precision over 99 errors out. The parser only reads two digits. Build the format string dynamically for anything bigger.
*is not supported. Noprintf("%*d", width, n)in Lua. Same workaround: build the format string.- Booleans are rejected by every specifier.
string.format("%s", true)errors. Usetostring(true)first, or convert to a string up front. nilraises an error rather than printing"nil". Guard missing values explicitly before passing them in.- No locale awareness. No thousand separators, no comma decimal. Output is locale-independent and machine-independent, which is usually what you want.
%uis misleading. It is not a true unsigned type. It just wraps negatives to 2^64 + n, so-1prints as18446744073709551615.- Floats with huge magnitude to
%d/%xerror when out of integer range.string.format("%d", 1e30)fails. Convert withmath.floorfirst, or use%f/%gfor large values. %conly accepts an integer — a float with a fractional part errors. Cast withmath.flooror convert explicitly.%%is required for a literal%. A bare%that is not part of a valid directive raises “invalid conversion specification”. This includes typos like%-s.%qdoes not accept non-strings in 5.4. Pass a string, or wrap withtostringfirst.- 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. - Hex floats are 5.4+ only.
%aand%Ado 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
string.find()— extract values to feed intoformatstring.gsub()— alternative way to build strings with capture replacementstring.byte()— inverse of%cstring.char()— alternative way to produce single characterstostring()— what%sultimately callstonumber()— round-trip with formatted numbers- Strings and Patterns tutorial
- Pattern Matching tutorial