Learn Lua in Neovim: Iterating Tables — pairs, ipairs, Gap Behavior & Word Counter | Episode 20
Video: Learn Lua in Neovim: Iterating Tables — pairs, ipairs, Gap Behavior & Word Counter | Episode 20 by Taught by Celeste AI - AI Coding Coach
Lua Iterating Tables: pairs, ipairs, and a Word Counter
ipairs(t)for arrays — walks 1, 2, 3 in order, stops at first nil.pairs(t)for any table — walks every key, unspecified order. Build a word-frequency counter.
pairs and ipairs are the two iteration primitives for tables. They look similar but behave differently. Today we cover both, the gap behaviour, and a small word-frequency example that uses tables-as-dicts.
ipairs: array iteration
local colors = { "red", "green", "blue", "yellow" }
for i, color in ipairs(colors) do
print(i .. " = " .. color)
end
Output:
1 = red
2 = green
3 = blue
4 = yellow
ipairs(t) walks integer keys starting at 1, in order, and stops at the first nil. Each iteration:
iis the key (1, 2, 3, ...).colorist[i].
Use ipairs when you have a sequence — values at consecutive integer keys.
pairs: dictionary iteration
local pet = {
name = "Max",
type = "dog",
age = 5,
}
for key, value in pairs(pet) do
print(key .. " = " .. tostring(value))
end
Output (order varies):
name = Max
type = dog
age = 5
pairs(t) walks every key in the table, regardless of type, in unspecified order. Each iteration:
keyis whatever key (string, number, etc.).valueist[key].
Use pairs when you have a dict (string keys) or a mixed table.
ipairs stops at gaps
local mixed = { "a", "b", nil, "d", "e" }
print("ipairs:")
for i, v in ipairs(mixed) do
print(i .. " = " .. v)
end
Output:
ipairs:
1 = a
2 = b
ipairs walks 1, 2, hits nil at 3, stops. Items at 4 and 5 are invisible to ipairs.
pairs would visit 1, 2, 4, 5 (skipping the deleted 3). Different semantics.
This is why Lua arrays should be gap-free — nil in the middle confuses ipairs and breaks #.
Order is unspecified for pairs
for k, v in pairs({a=1, b=2, c=3}) do
print(k, v)
end
The output order is implementation-defined. Lua does not preserve insertion order, and the order can change between Lua versions or even runs.
For predictable order, collect keys into a list and sort:
local t = {b=1, a=2, c=3}
local keys = {}
for k in pairs(t) do keys[#keys + 1] = k end
table.sort(keys)
for _, k in ipairs(keys) do
print(k, t[k])
end
Verbose but deterministic.
Word frequency counter
local words = { "lua", "is", "fun", "lua", "is", "great", "lua" }
local counts = {}
for i = 1, #words do
local word = words[i]
if counts[word] then
counts[word] = counts[word] + 1
else
counts[word] = 1
end
end
for word, count in pairs(counts) do
print(word .. ": " .. count)
end
Output:
lua: 3
is: 2
fun: 1
great: 1
Two passes:
- Build the counts. Walk the words. If the count exists, increment; else, initialise to 1.
- Print the counts.
pairswalks the dict, prints each key-value.
The count-or-init pattern is so common it has a shorthand:
counts[word] = (counts[word] or 0) + 1
(x or 0) returns x if truthy, else 0. The whole expression then increments cleanly.
Loop variable is throwaway
for _, v in ipairs(items) do
print(v)
end
If you don't need the index, _ is the conventional throwaway. Same for keys:
for _, v in pairs(t) do
print(v) -- only values, ignore keys
end
for k, _ in pairs(t) do
print(k) -- only keys, ignore values
end
Iterators are functions
for k, v in pairs(t) do ... end
Under the hood, pairs(t) returns three things: an iterator function, the table, and an initial control value. The for loop calls the iterator repeatedly. You can write your own iterators — covered in the Programming in Lua book.
For most code, you only need pairs, ipairs, and the numeric for.
Iterating in array order with a non-array
local sparse = { [3] = "c", [1] = "a", [2] = "b" }
for i, v in ipairs(sparse) do
print(i .. " = " .. v)
end
Output:
1 = a
2 = b
3 = c
Even though we set keys out of order, ipairs still walks 1, 2, 3 in order. The internal storage may rearrange, but ipairs sees the logical sequence.
Common stumbles
Using ipairs on a table with non-integer keys. Won't see them — ipairs only walks integer keys 1..n. Use pairs for dicts.
Relying on pairs order. Implementation-defined. Sort if you need order.
Modifying the table during iteration. Setting an existing key to a new value is fine. Adding new keys during pairs is undefined behaviour. Removing keys (setting to nil) is iffy — some keys might be skipped.
Trying to use # on a dict. #{a=1, b=2} is 0 because there's no array part. # only counts integer keys 1..n.
Confusing pairs(t) with t.pairs(t). pairs is a global function, not a method. Just pairs(t), no dot.
What's next
Episode 21: nested tables. Tables containing tables — a student record with grades, a classroom with students. Chained access (student.address.city) and computing averages.
Recap
ipairs(t) for arrays — in-order, stops at first nil. pairs(t) for any table — every key, unspecified order. Use _ for throwaway loop variables. The (x or 0) + 1 idiom for count-or-init. For order-preserving iteration, collect and sort keys. Don't add keys during pairs; don't nil keys during ipairs.
Next episode: nested tables.