Part of Learn Lua with NeoVim

Learn Lua in Neovim: Iterating Tables — pairs, ipairs, Gap Behavior & Word Counter | Episode 20

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Iterating Tables — pairs, ipairs, Gap Behavior & Word Counter | Episode 20 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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:

  • i is the key (1, 2, 3, ...).
  • color is t[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:

  • key is whatever key (string, number, etc.).
  • value is t[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-freenil 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:

  1. Build the counts. Walk the words. If the count exists, increment; else, initialise to 1.
  2. Print the counts. pairs walks 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.