Lua with Neovim: Nested Tables — Tables Inside Tables, Chained Access & Averages | Episode 21
Video: Lua with Neovim: Nested Tables — Tables Inside Tables, Chained Access & Averages | Episode 21 by Taught by Celeste AI - AI Coding Coach
Lua Nested Tables: Tables Inside Tables
A student with a list of grades and an address. A classroom of students. Chained access (
student.address.city) and computing class averages from nested data.
Tables can contain other tables — that's how you build records with sub-records, lists of objects, and tree-structured data. Today we put it all together.
A student record
local student = {
name = "Alice",
grades = { 90, 85, 92, 88 },
address = {
street = "123 Main St",
city = "Portland",
},
}
print("Name: " .. student.name)
print("First grade: " .. student.grades[1])
print("City: " .. student.address.city)
Output:
Name: Alice
First grade: 90
City: Portland
Three fields:
name— a string.grades— an array of numbers (table with integer keys).address— a dict with string keys.
Access chains naturally with dots and brackets:
student.grades[1]— read grades, then take index 1.student.address.city— read address, then read city.
Modifying nested fields
student.address.city = "Seattle"
print("New city: " .. student.address.city)
Same syntax. The chain works for both reading and writing.
To add a new nested field:
student.address.zip = "98101"
To replace the whole sub-table:
student.address = {
street = "456 Oak Ave",
city = "Spokane",
}
A classroom: list of students
local classroom = {
{
name = "Alice",
grades = { 90, 85, 92 },
},
{
name = "Bob",
grades = { 78, 82, 80 },
},
{
name = "Charlie",
grades = { 95, 98, 91 },
},
}
classroom is an array of student records. Index it numerically:
print(classroom[1].name) -- "Alice"
print(classroom[2].grades[1]) -- 78 (Bob's first grade)
Computing averages
for i = 1, #classroom do
local s = classroom[i]
local total = 0
for j = 1, #s.grades do
total = total + s.grades[j]
end
local avg = total / #s.grades
print(s.name .. " average: " .. string.format("%.1f", avg))
end
Output:
Alice average: 89.0
Bob average: 80.0
Charlie average: 94.7
Nested loops:
- Outer: walk students.
- Inner: walk each student's grades.
Sum the grades, divide by count, format to one decimal place.
string.format("%.1f", avg) produces a clean fixed-decimal output.
Cleaner with ipairs
for _, s in ipairs(classroom) do
local total = 0
for _, grade in ipairs(s.grades) do
total = total + grade
end
print(s.name .. " avg: " .. (total / #s.grades))
end
Same logic. ipairs skips the index when you don't need it.
Tree shapes
local org = {
name = "Acme Corp",
ceo = {
name = "Alice",
reports = {
{ name = "Bob", role = "Engineering" },
{ name = "Carol", role = "Sales" },
},
},
}
print(org.ceo.reports[1].name) -- "Bob"
print(org.ceo.reports[2].role) -- "Sales"
Arbitrarily deep nesting, accessed by chained dots and brackets. The shape of the data is whatever you build.
Adding to a nested array
table.insert(classroom[1].grades, 100) -- Alice gets a 100
table.insert works on any array, including one nested deep inside another structure. Same for table.remove.
Iterating into nested tables
To walk every grade across all students:
for _, s in ipairs(classroom) do
for _, g in ipairs(s.grades) do
print(s.name .. ": " .. g)
end
end
Two ipairs loops. Lua doesn't have a "deep iteration" built-in — you write the nested loops explicitly.
JSON-shaped data
Lua tables are essentially JSON's data model. Both have arrays, dictionaries, and arbitrary nesting. Many JSON libraries (e.g. dkjson, cjson) decode JSON straight into Lua tables:
local json = require("dkjson")
local data = json.decode([[
{ "name": "Alice", "grades": [90, 85] }
]])
print(data.name) -- "Alice"
print(data.grades[1]) -- 90
Same access patterns as before. Tables are flexible enough that JSON's data needs no transformation.
Watch for nil
print(student.address.zip)
-- prints "nil" if zip wasn't set; doesn't crash
print(student.address.zip.country)
-- ERROR: tries to index nil
The first line is fine — reading a missing field returns nil. The second crashes — you can't index nil.
Safe-chain pattern:
local zip = student.address and student.address.zip
local country = zip and zip.country
Or the inline:
local country = (student.address and student.address.zip and student.address.zip.country) or "unknown"
Each and short-circuits if the previous part is nil. Verbose for deep paths.
Pretty-printing nested tables
Lua's built-in print shows table addresses (table: 0x...), not contents. For real inspection, write a small printer or use a library:
local function dump(t, indent)
indent = indent or ""
for k, v in pairs(t) do
if type(v) == "table" then
print(indent .. tostring(k) .. " = {")
dump(v, indent .. " ")
print(indent .. "}")
else
print(indent .. tostring(k) .. " = " .. tostring(v))
end
end
end
dump(student)
For production, use require("inspect") or require("dkjson").encode(t, {indent=true}).
Common stumbles
Indexing nil. t.a.b.c crashes if t.a is nil. Defensive: t.a and t.a.b and t.a.b.c.
Forgetting tables are reference types. Two tables with identical contents are not equal — equality is by reference (covered next episode).
Modifying through one variable, surprised the other changed. Two variables can point to the same table; mutations are visible through both. Episode 22 makes this explicit.
Using # on the wrong level. #classroom is the number of students. #classroom[1].grades is the number of grades for student 1. Each level has its own length.
Mixing array and dict in the same table without intent. {a=1, "first"} is two tables in one — t.a and t[1]. Easy to confuse.
What's next
Episode 22: table references vs copies. The "two variables, one table" trap. Shallow copy with pairs. Deep copy via recursion. Why arrays-of-arrays are sneaky.
Recap
Tables can contain other tables — arrays of records, records of records, trees of any shape. Access via chained . and []. Read missing fields returns nil; indexing through a nil crashes. ipairs/pairs only iterate one level — write nested loops for deeper traversal. Lua's table model is JSON-shaped, making JSON interop trivial.
Next episode: references vs copies.