luaguides

pairs

pairs() is a stateless iterator for traversing Lua tables. Unlike ipairs(), it iterates over all key-value pairs including non-sequential keys, and it correctly handles nil values. Internally, pairs() uses next() to walk the table’s hash part.

Basic Usage

t = {a = 1, b = 2, c = 3}

for k, v in pairs(t) do
    print(k, v)
end
-- c 3
-- b 2
-- a 1

The iteration order is not guaranteed for hash tables — Lua uses the hash bucket order, which can vary between runs. This means you should never rely on pairs producing keys in a specific sequence, even if you observe the same order across multiple runs of the same program on the same Lua version. For predictable ordering, collect keys into an array and sort them first.

The table below contrasts pairs with ipairs, which is the other standard iterator. Knowing when to use each one prevents subtle bugs, especially when dealing with tables that mix array and dictionary entries.

pairs vs ipairs

Behaviorpairsipairs
Iterates nil valuesYesNo (stops at first nil)
Preserves insertion orderNoYes (sequential indices)
Works with non-sequential keysYesNo
Suitable for sparse arraysNoYes (with caution)
-- pairs handles nil values
t = {1, nil, 3, k = "value"}
for i, v in pairs(t) do
    print(i, v)
end
-- 1    1
-- 3    3
-- k    value

-- ipairs stops at first nil
for i, v in ipairs(t) do
    print(i, v)
end
-- 1    1

Using next directly

The comparison shows pairs navigating past the nil at index 2 while ipairs stops dead. Under the hood, pairs(t) is syntactic sugar for next, t, nil — it returns the next function, the table, and an initial nil key. You can call next directly when you need finer control over the iteration loop, such as breaking mid-iteration or advancing the cursor conditionally without the rigid structure of a for loop.

-- Iterate without creating an iterator variable
local k
while true do
    k = next(t, k)
    if k == nil then break end
    print(k, t[k])
end

Iterating sparse tables

The direct next loop gives you manual cursor control, but pairs itself handles the most common iteration scenarios cleanly. One area where pairs excels over ipairs is sparse tables — tables with large gaps between indices. Since ipairs stops at the first nil in a contiguous sequence, it misses entries at higher indices. pairs, however, visits every key that exists, regardless of gaps, making it the right choice for sparse data structures like sparse matrices or lookup tables built with arbitrary integer keys.

sparse = {}
sparse[1] = "first"
sparse[100] = "hundredth"
sparse[50] = "fiftieth"

for k, v in pairs(sparse) do
    print(k, v)
end
-- 1     first
-- 50    fiftieth
-- 100   hundredth

Modifying tables during iteration

Sparse iteration works because pairs does not assume sequential keys — it simply asks next for whatever key follows the current one in the internal hash table. This flexibility has a downside, though. Modifying a table while iterating over it with pairs is dangerous: removing the current key can confuse the internal iterator state, causing some keys to be skipped and others to appear twice. Lua’s manual explicitly warns that the behaviour is undefined when you delete keys during iteration.

t = {a = 1, b = 2, c = 3}

for k, v in pairs(t) do
    t.d = v + 1  -- safe: adds new key
    if k == "b" then
        t[k] = nil  -- unsafe: may skip or repeat keys
    end
end

The unsafe example deletes key "b" mid-iteration, which may cause "c" to be skipped or visited erratically depending on the hash bucket layout. Adding new keys, however, is generally safe — new entries go into buckets that the iterator may or may not reach, but they will not corrupt the existing traversal state.

For safe deletion, collect the keys you want to remove into a separate list during the first pass, then delete them in a second pass. This two-pass approach guarantees every key is visited exactly once before any deletions take effect.

for k, v in pairs(t) do
    if some_condition then
        t[k] = nil
    end
end
-- Safer: collect keys first
local keys = {}
for k in pairs(t) do table.insert(keys, k) end
for _, k in ipairs(keys) do
    if condition then t[k] = nil end
end

Checking if a table is empty

The two-pass deletion pattern handles the most dangerous case of table mutation during iteration. A simpler and safer use of the iterator primitives is checking whether a table has any entries at all. Since next(t) returns the first key-value pair in a non-empty table, comparing its result to nil gives you a fast, zero-allocation emptiness test that works for both array-like and dictionary-like tables — unlike #t == 0, which fails for tables with non-sequential keys.

function is_empty(t)
    return next(t) == nil
end

is_empty({})           -- true
is_empty({a = 1})    -- false

Common Patterns

The is_empty check uses next directly — a concise, zero-allocation way to test whether a table has any entries. Beyond emptiness checks, pairs enables several recurring patterns in Lua code. The three sub-sections below cover the most common ones: iterating over keys only, iterating over values only, and using pairs to build a deep copy of a nested table structure.

Iterating keys only

for k in pairs(t) do
    print(k)
end

Iterating values only

When you only need the keys, the first return value is discarded with _. The opposite case — iterating over values and ignoring keys — is equally common. Dropping the key with _ keeps the loop clean and signals to readers that the iteration order does not matter for the operation at hand. This pattern appears frequently in data transformation pipelines and validation loops.

for _, v in pairs(t) do
    print(v)
end

Deep copy

Iterating values is useful for flat transformations, but when you need to duplicate an entire nested table structure, pairs combined with recursion handles it cleanly. The deep-copy pattern visits every key-value pair recursively — if a value is itself a table, the function calls itself on that sub-table before inserting it into the copy. This handles arbitrary nesting depth, though it will overflow the stack on extremely deep structures.

function deep_copy(obj)
    if type(obj) ~= "table" then return obj end
    local copy = {}
    for k, v in pairs(obj) do
        copy[deep_copy(k)] = deep_copy(v)
    end
    return copy
end

See Also