table.pack
table.pack(...): table Syntax
table.pack(...)
Parameters
... — variadic. Any number of values from zero upward. Each value is stored in the returned table at integer keys 1, 2, and so on, in the order the arguments were given. nil arguments are accepted; they remain as nil at their slot, but they break sequence semantics, so the resulting table is not always a sequence.
Return value
A single table. Integer keys 1 through n hold the arguments, where n equals the number of arguments passed. The table also has a field n set to that same count, so you can read it as t.n or via select("#", ...). Even with zero arguments the result is a non-empty table: table.pack() returns { n = 0 }.
Description
table.pack captures a Lua function’s variadic arguments into a table that knows its own length. The classic alternative {...} builds a sequence but loses information about trailing nils, because #t returns the length up to the first nil boundary rather than the actual argument count. table.pack fixes that by writing the count into t.n regardless of whether any arguments were nil.
The function is part of the standard library since Lua 5.2 and is unchanged in Lua 5.3 and 5.4. Its main use cases are capturing varargs so they can be inspected or logged, forwarded to another function later, or stored alongside metadata such as timestamps and labels.
Examples
Example 1: Basic packing and reading back
local args = table.pack("a", 10, true, { 1, 2 })
print(args.n) -- 4
print(args[1], args[2], args[3]) -- a 10 true
print(type(args[4])) -- table
print(#args) -- 4
This shows the basic shape of a table.pack result: integer keys hold the values in order, t.n records the argument count, and the table can contain any Lua value including nested tables and functions. Because the nested table at args[4] is stored by reference rather than copied, mutating it later would also change the original value passed in.
Example 2: Trailing nils — why t.n exists
local function demo(...)
local packed = table.pack(...)
local listwise = {...}
print(packed.n, #listwise)
end
demo("x", nil, "y") -- 3 3
demo(nil, nil, "z") -- 3 0
demo("a", "b", nil, nil) -- 4 2
packed.n always reports the real argument count; #listwise does not. The second call shows the worst case: leading nils make #t report 0 even when three arguments were passed. This is the reason table.pack exists rather than just using {...} everywhere, and the same reason select("#", ...) is preferred over #{...} when you need a reliable count.
Example 3: Round-trip with table.unpack
local function var(...)
return table.pack(...)
end
local t = var(1, 2, 3)
local a, b, c = table.unpack(t, 1, t.n)
print(a, b, c) -- 1 2 3
Pass 1, t.n explicitly so the round trip stays correct even if the table is mutated later. table.unpack defaults its end index to #t, which can disagree with t.n once you edit the table.
Example 4: Forwarding variadic arguments
local function trace(label, ...)
local args = table.pack(...)
for i = 1, args.n do
io.write(label, "[", i, "]=", tostring(args[i]), "\t")
end
io.write("\n")
return table.unpack(args, 1, args.n)
end
trace("> ", 1, "two", { three = 3 })
-- > [1]=1 [2]=two [3]=table: 0x...
This is the canonical pattern: capture varargs to inspect or log them, then forward them downstream with table.unpack(args, 1, args.n). The explicit range keeps the forward correct even after edits to the captured table.
Example 5: Zero arguments
local t = table.pack()
print(t.n, #t) -- 0 0
print(next(t) == nil) -- false (the table has the "n" field)
The table is empty by sequence semantics but not empty as a map; next(t) finds the "n" key. This matters when you check truthiness: if table.pack() then is always true, so use t.n > 0 or select("#", ...) > 0 to test whether any arguments were actually passed.
Example 6: Iterating with t.n versus ipairs
local original = table.pack(10, 20, nil, 40)
local count_via_ipairs = 0
for _ in ipairs(original) do count_via_ipairs = count_via_ipairs + 1 end
print(count_via_ipairs) -- 3
local count_via_n = 0
for i = 1, original.n do count_via_n = count_via_n + 1 end
print(count_via_n) -- 4
ipairs stops at the first nil; a numeric loop bound by t.n walks every position, including the nil at index 3. This is the practical rule: use ipairs when you want sequence semantics and have no nils, and use for i = 1, t.n when you must visit every slot including holes.
Common mistakes
- Trusting
#tafter packing. When the packed arguments contain nil,#treturns the array boundary, which can be smaller thant.n. Uset.nto bound loops when nil arguments are possible. - Mutating
t.nto “trim” the table. Settingt.n = t.n - 1does not drop the last element, andtable.unpackdefaults its end index to#t, nott.n. Usetable.remove(t)to actually shorten the table. - Treating the result as falsy when there are no arguments.
table.pack()returns a non-empty table withn = 0, soif table.pack(...) thenis always true. Testargs.n > 0orselect("#", ...) > 0instead. - Confusing
table.packwithstring.pack. The latter is a binary struct packer with totally different semantics. Same word, different module. - Allocating on a hot path.
table.packalways creates a new table. For code that packs thousands of varargs in a tight loop, reuse a buffer or useselectto read the count without allocating.
See also
- select —
select("#", ...)returns the same count ast.nwithout allocating. - unpack — the Lua 5.1 global name that became
table.unpack. - table.insert — add elements to a sequence at a position.
- table.remove — drop elements from a sequence.
- Arrays and lists — how tables represent list-like structures.