luaguides

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 #t after packing. When the packed arguments contain nil, #t returns the array boundary, which can be smaller than t.n. Use t.n to bound loops when nil arguments are possible.
  • Mutating t.n to “trim” the table. Setting t.n = t.n - 1 does not drop the last element, and table.unpack defaults its end index to #t, not t.n. Use table.remove(t) to actually shorten the table.
  • Treating the result as falsy when there are no arguments. table.pack() returns a non-empty table with n = 0, so if table.pack(...) then is always true. Test args.n > 0 or select("#", ...) > 0 instead.
  • Confusing table.pack with string.pack. The latter is a binary struct packer with totally different semantics. Same word, different module.
  • Allocating on a hot path. table.pack always creates a new table. For code that packs thousands of varargs in a tight loop, reuse a buffer or use select to read the count without allocating.

See also

  • selectselect("#", ...) returns the same count as t.n without 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.