Arrays, dictionaries, namespaces, and modules — all built on a single data structure. Master tables and you understand 80% of Lua.
A table in Lua is an associative array: it maps keys to values. Keys can be any Lua value except nil and NaN. This one data structure replaces arrays, dictionaries, sets, objects, namespaces, and modules in every other language.
ipairs vs pairs, table.* library functions, nested tables, and the module pattern.-- Empty table
local t = {}
-- Dictionary-style: string keys
local person = {
name = "Alice",
age = 30,
email = "[email protected]",
}
-- Access with dot notation or bracket notation
print(person.name) --> Alice
print(person["age"]) --> 30
-- Add or update keys
person.city = "Denver"
person["age"] = 31
-- Remove a key (set to nil)
person.email = nil
-- Any value (except nil/NaN) can be a key
local t2 = {}
t2[1] = "one"
t2[true] = "yes"
t2[3.14] = "pi"
-- Tables are passed by reference
local a = {x = 1}
local b = a -- b points to the same table
b.x = 99
print(a.x) --> 99 (not a copy!)
When you use integer keys starting at 1, Lua treats the table like an array. The # length operator returns the last integer key in the sequence (it assumes no gaps).
-- Array constructor
local fruits = {"apple", "banana", "cherry", "date"}
print(#fruits) --> 4
print(fruits[1]) --> apple (1-indexed!)
print(fruits[#fruits]) --> date (last element)
-- table.insert: append or insert at position
table.insert(fruits, "elderberry") -- append
table.insert(fruits, 2, "avocado") -- insert at index 2
-- table.remove: remove at position (default: last)
local removed = table.remove(fruits, 2) -- removes "avocado"
print(removed) --> avocado
-- table.concat: join to string
print(table.concat(fruits, ", "))
--> apple, banana, cherry, date, elderberry
-- table.sort: in-place sort
table.sort(fruits)
print(table.concat(fruits, ", "))
--> apple, banana, cherry, date, elderberry (alphabetical)
-- Custom sort comparator
table.sort(fruits, function(a, b) return #a < #b end)
print(table.concat(fruits, ", "))
--> date, apple, banana, cherry, elderberry (by length)
-- WARNING: #t is unreliable if the array has gaps (holes)
local sparse = {1, 2, nil, 4}
print(#sparse) -- undefined behavior: could be 2 or 4
ipairs iterates over integer keys 1, 2, 3... stopping at the first nil. pairs iterates over all key-value pairs in arbitrary order. Use ipairs for arrays, pairs for dictionaries.
local mixed = {10, 20, 30, name="Alice", city="Denver"}
-- ipairs: only integer keys 1, 2, 3 ...
print("ipairs:")
for i, v in ipairs(mixed) do
print(i, v)
end
-- 1 10
-- 2 20
-- 3 30
-- (does NOT print name or city)
-- pairs: ALL key-value pairs, any order
print("pairs:")
for k, v in pairs(mixed) do
print(k, v)
end
-- 1 10
-- 2 20
-- 3 30
-- name Alice
-- city Denver
-- next(): the raw iterator used by pairs
-- useful for checking if a table is empty
local function is_empty(t)
return next(t) == nil
end
print(is_empty({})) --> true
print(is_empty({1,2,3})) --> false
-- Nested tables
local config = {
server = {
host = "localhost",
port = 8080,
tls = false,
},
database = {
url = "postgres://localhost/mydb",
pool = 10,
timeout = 30,
},
}
print(config.server.port) --> 8080
print(config.database.pool) --> 10
-- Shallow copy
local function shallow_copy(t)
local copy = {}
for k, v in pairs(t) do copy[k] = v end
return copy
end
-- Deep copy (recursive)
local function deep_copy(orig)
local copy
if type(orig) == "table" then
copy = {}
for k, v in pairs(orig) do
copy[deep_copy(k)] = deep_copy(v)
end
setmetatable(copy, getmetatable(orig))
else
copy = orig
end
return copy
end
local orig = {a = {1, 2, 3}, b = "hello"}
local c = deep_copy(orig)
c.a[1] = 99
print(orig.a[1]) --> 1 (not affected)
Lua doesn't have a built-in module system but tables make it trivial to build one. The standard pattern is to return a table from a file and require it elsewhere.
-- math_utils.lua (this is the module file)
local M = {} -- 'M' is conventional for the module table
local function clamp(v, lo, hi) -- private: not exported
return math.min(math.max(v, lo), hi)
end
function M.lerp(a, b, t)
t = clamp(t, 0, 1)
return a + (b - a) * t
end
function M.sign(x)
if x > 0 then return 1
elseif x < 0 then return -1
else return 0
end
end
function M.round(x, decimals)
local factor = 10 ^ (decimals or 0)
return math.floor(x * factor + 0.5) / factor
end
return M -- MUST return the module table
-- ─────────────────────────────────────────
-- main.lua (consumer)
local mu = require("math_utils")
print(mu.lerp(0, 100, 0.25)) --> 25.0
print(mu.sign(-7)) --> -1
print(mu.round(3.14159, 2)) --> 3.14
require caches modules. Calling require("math_utils") multiple times returns the same table — Lua only runs the file once and stores the result in package.loaded. This is safe and efficient.stack.lua with a new() function that returns a stack object. The stack should have push(v), pop(), peek(), size(), and is_empty() methods — all backed by a table.queue.lua similarly with enqueue(v), dequeue(), front(), size(), and is_empty(). Use two indices (head and tail) to avoid O(n) removal.to_array() method to both that returns all current elements as a plain Lua array.stack.new() to accept an optional initial array and pre-populate the stack from it.Implement a Set module using tables. Support new(list), add(v), remove(v), contains(v), union(s1, s2), intersection(s1, s2), and difference(s1, s2). Use boolean table values (t[v] = true) for O(1) membership testing.
The foundations from today carry directly into Day 3. In the next session the focus shifts to Day 3 — building directly on everything covered here.
Before moving on, verify you can answer these without looking:
Live Bootcamp
Learn this in person — 2 days, 5 cities
Thu–Fri sessions in Denver, Los Angeles, New York, Chicago, and Dallas. $1,490 per seat. June–October 2026.
Reserve Your Seat →