Tables: Lua's Core Data Structure
Master Lua's single, all-purpose data structure — array-style and key-style tables, table.insert/remove, the length operator, and pairs vs ipairs.
Introduction
This is the single most important lesson in this course. Lua has exactly one composite data structure — the table — and it does the job that arrays, dictionaries, sets, objects, and even modules handle separately in most other languages. Every later lesson in this course, from functions to metatables to modules, builds directly on what you learn here.
- How to build and use tables as arrays (sequential, integer-indexed lists).
- How to build and use the exact same table type as a dictionary (string or arbitrary keys).
- How to insert, remove, and iterate over table contents correctly.
- Why the # length operator has real caveats you need to know about.
Tables as Arrays
A table built with values in curly braces, separated by commas, becomes an array-style table — automatically indexed starting at 1, not 0. This 1-based indexing is one of Lua's most distinctive and most-discussed design choices.
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- apple (indexing starts at 1, not 0!)print(fruits[3]) -- cherryprint(#fruits) -- 3 (the length operator)Click Run to see what this code prints.
Tables as Dictionaries
The exact same table type can be built with explicit keys instead, turning it into what most languages call a dictionary, map, or object.
local person = { name = "Priya", age = 29, city = "Bengaluru",}
print(person.name) -- dot syntax for known string keysprint(person["age"]) -- bracket syntax works too, useful for dynamic keys
person.age = 30 -- tables are mutableprint(person.age)Click Run to see what this code prints.
person.name is just shorthand for person["name"] — the dot syntax only works when the key is a valid identifier known at write time. Use bracket syntax whenever the key is itself a variable, like person[keyVariable].
Mixed and Nested Tables
Tables can freely mix array-style and key-style entries in the same table, and tables can contain other tables — this is how Lua represents any nested or structured data, from JSON-like configuration to game state.
local student = { "extra-note", -- becomes index [1] name = "Arjun", scores = {88, 92, 79}, -- a nested table}
print(student[1]) -- extra-noteprint(student.name) -- Arjunprint(student.scores[2]) -- 92Click Run to see what this code prints.
table.insert and table.remove
The standard library's table functions add and remove elements from array-style tables while keeping indices consistent — far safer than manually shifting indices yourself.
local queue = {"first", "second"}
table.insert(queue, "third") -- appends to the endtable.insert(queue, 1, "zeroth") -- inserts at position 1, shifting the rest
for i, item in ipairs(queue) do print(i, item)end
table.remove(queue, 1) -- removes "zeroth"print("After remove:", queue[1])Click Run to see what this code prints.
pairs vs ipairs
Lua has two built-in iteration functions for tables, and choosing the wrong one is a very common early mistake. ipairs() iterates only the array part, in order, stopping at the first nil — use it for sequential lists. pairs() iterates every key in the table, in no guaranteed order — use it for dictionary-style tables or when you need everything.
local mixed = {"a", "b", "c", x = 1, y = 2}
print("-- ipairs --")for i, v in ipairs(mixed) do print(i, v)end
print("-- pairs --")for k, v in pairs(mixed) do print(k, v)endClick Run to see what this code prints.
The Length Operator and Its Caveats
The # operator returns a "border" of the array part of a table — for a clean, hole-free array like {1, 2, 3}, that's exactly its length. But if the array has gaps (a nil in the middle), # is explicitly allowed to return any valid border, which can produce surprising or inconsistent results.
local clean = {"a", "b", "c"}print(#clean) -- 3, reliable
local withHole = {"a", nil, "c"}print(#withHole) -- could print 1 or 3 -- undefined with holes, avoid this shapeClick Run to see what this code prints.
Common Mistakes
- Assuming tables are 0-indexed like arrays in most other languages — Lua tables start at index 1.
- Using pairs() on a pure array when you need guaranteed order — use ipairs() instead.
- Creating "holes" in an array-style table (by setting a middle element to nil) and then relying on # — the result is explicitly undefined in that case.
- Forgetting that assigning nil to a table key removes that key entirely — it does not create a "null" entry the way some languages do.
Best Practices
- Use table.insert() and table.remove() instead of manually shifting indices by hand.
- Reach for ipairs() on ordered lists and pairs() on dictionary-style tables — pick deliberately, don't default to one out of habit.
- Avoid creating holes in array-style tables; if you need to represent "missing," use a dedicated sentinel value instead of nil in the middle of a sequence.
- Remember the 1-based indexing rule explicitly when translating algorithms from a 0-indexed language.
Frequently Asked Questions
It's a deliberate "mechanism, not policy" design choice — one flexible, general-purpose structure keeps the language small while still covering arrays, dictionaries, objects (lesson 9), and modules (lesson 11).
The array part (sequential integer keys starting at 1) is ordered. The hash/dictionary part iterated by pairs() has no guaranteed order.
There's no built-in isEmpty() — the idiomatic check is next(myTable) == nil, since next() returns nil when there are no more (or no) entries to iterate.
Key Takeaways
- Tables are Lua's single, all-purpose data structure — arrays, dictionaries, and more all use the same type.
- Array-style tables are 1-indexed, not 0-indexed.
- table.insert() and table.remove() safely modify array-style tables while keeping indices correct.
- ipairs() iterates the ordered array part; pairs() iterates every key with no guaranteed order.
- The # length operator is only reliable on hole-free array-style tables.
Summary
Tables are the foundation everything else in Lua is built on — you'll use exactly what you learned here in every remaining lesson. Next, you'll cover control flow: if/else, loops, and how Lua handles branching and repetition.