Metatables and Metamethods
Reviewed & published by Brayan K
By the end of this lesson you'll use tables as both arrays and dictionaries, customise how they behave with metatables and metamethods, and build your own classes with inheritance — the foundation of every serious Lua program.
Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
- Use a table as an array and a dictionary (1-based indexing, #, insert/remove)
- Attach a metatable with setmetatable and getmetatable
- Provide defaults and inheritance with the __index metamethod
- Intercept writes with __newindex
- Overload operators with __add, __eq, and __tostring
- Build classes with :method() colon syntax, self, and inheritance
💡 Real-World Analogy
Think of a table as a sheet of paper holding your data, and a metatable as a sticky note of instructions clipped to it. Normally Lua just reads the paper. But when something unusual happens — you ask for a row that isn't written down, or you try to add two sheets together — Lua reads the sticky note to find out what to do. The sticky note doesn't store data; it stores behaviour. That single idea — "look at the manual when you hit something unexpected" — is how Lua delivers default values, operator overloading, and full object-oriented programming, all from one humble data type.
1. Tables: Lua's One Data Structure
Lua has exactly one built-in data structure: the table. Remarkably, the same table is both an array (a numbered list) and a dictionary (named key → value pairs). The one rule that trips up newcomers: Lua arrays start at index 1, not 0. You add and remove with table.insert and table.remove, and you measure length with the # operator. Read this worked example and run it before moving on.
-- A table is Lua's ONLY data structure. It is both an
-- array (numbered list) AND a dictionary (key -> value) at once.
-- ARRAY part: a list. Lua arrays start at index 1, NOT 0.
local fruits = { "apple", "banana", "cherry" }
print(fruits[1]) -- prints: apple (first item is [1])
print(fruits[3]) -- prints: cherry
print(#fruits) -- prints: 3 (# is the length operator)
-- table.insert adds to the end; table.remove takes from the end.
table.insert(fruits, "date")
print(#fruits) -- prints: 4
table.remove(fruits, 1) -- remove index 1 ("apple"); rest shift down
print(fruits[1]) -- prints: banana
-- DICTIONARY part: string keys instead of numbers.
local player = { name = "Hero", level = 5, alive = true }
print(player.name) -- prints: Hero (dot syntax)
print(player["level"]) -- prints: 5 (bracket syntax — same thing)
-- One table can hold BOTH parts at the same time.
local mixed = { "first", "second", color = "red" }
print(mixed[1]) -- prints: first (array part)
print(mixed.color) -- prints: red (hash part)Your turn. The snippet below is almost complete — fill in the three ___ blanks using the hints, then run it and check it against the ✅ Expected output comments.
-- 🎯 YOUR TURN — replace each ___, then run this on onecompiler.com.
local scores = { 10, 20, 30 }
-- 1) Print the FIRST score (remember: Lua arrays start at 1)
print(scores[___]) -- 👉 the index of the first item
-- ✅ Expected output: 10
-- 2) Add 40 to the end of the list
table.insert(scores, ___) -- 👉 the number to add
-- 3) Print how many scores there are now
print(___scores) -- 👉 the length operator goes before the table
-- ✅ Expected output: 42. Metatables & Metamethods
A metatable is an ordinary table whose special keys — called metamethods — tell Lua what to do in situations it doesn't otherwise handle. You attach one with setmetatable(t, mt) and read it back with getmetatable(t). The most important metamethod is __index: when you read a key the table doesn't have, Lua consults __index instead of giving you nil — perfect for defaults and inheritance. Others let you intercept writes (__newindex) or redefine operators like + (__add), == (__eq), and printing (__tostring).
Key Metamethods
| Metamethod | Triggered when |
|---|---|
| __index | You read a missing key |
| __newindex | You set a new (missing) key |
| __add / __sub / __mul | The + / - / * operators |
| __eq / __lt / __le | The == / < / <= operators |
| __tostring | tostring() or print() |
-- A metatable is a table that customises how ANOTHER table behaves.
-- setmetatable(t, mt) attaches the metatable mt to the table t.
-- __index supplies DEFAULTS: when a key is missing on the table,
-- Lua looks it up in __index instead of returning nil.
local defaults = { color = "red", speed = 10, health = 100 }
local player = setmetatable({ name = "Hero", speed = 25 }, { __index = defaults })
print(player.name) -- prints: Hero (own field)
print(player.speed) -- prints: 25 (own field wins over the default)
print(player.color) -- prints: red (missing -> falls back via __index)
print(player.health) -- prints: 100 (from defaults)
-- __newindex intercepts SETTING a new key. Here we block writes
-- to a "read-only" config by raising an error.
local config = setmetatable({}, {
__newindex = function(t, key, value)
error("config is read-only: cannot set " .. key)
end
})
-- config.volume = 11 -- would raise: config is read-only: cannot set volume
-- __add overloads +, __eq overloads ==, __tostring customises printing.
local Vector = {}
Vector.__index = Vector
Vector.__add = function(a, b)
return setmetatable({ x = a.x + b.x, y = a.y + b.y }, Vector)
end
Vector.__eq = function(a, b)
return a.x == b.x and a.y == b.y
end
Vector.__tostring = function(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local v1 = setmetatable({ x = 3, y = 4 }, Vector)
local v2 = setmetatable({ x = 1, y = 2 }, Vector)
print(tostring(v1 + v2)) -- prints: (4, 6) (uses __add then __tostring)
print(v1 == v1) -- prints: true (uses __eq)
print(v1 == v2) -- prints: falseNow you try overloading operators. Finish the Money type below so that + adds two amounts and print shows a £ sign. Fill in the two blanks:
-- 🎯 YOUR TURN — finish the Money type so + adds amounts.
local Money = {}
Money.__index = Money
-- 1) __add should return a new Money whose amount is the sum
Money.__add = function(a, b)
return setmetatable({ amount = a.amount ___ b.amount }, Money)
-- 👉 the arithmetic operator that adds two numbers
end
-- 2) __tostring should print the amount with a £ sign in front
Money.__tostring = function(m)
return "£" .. ___ -- 👉 the field that holds the number
end
local wallet = setmetatable({ amount = 30 }, Money)
local payment = setmetatable({ amount = 12 }, Money)
print(tostring(wallet + payment)) -- ✅ Expected output: £42Deep Dive: __index as a table vs a function
__index can be a table (the usual case — Lua looks the missing key up inside it) or a function that Lua calls as __index(table, key) to compute a value on demand.
-- function form: compute or log on a missing key
local t = setmetatable({}, {
__index = function(tbl, key)
return "no value for " .. key -- instead of nil
end
})
print(t.banana) -- prints: no value for bananaUse the table form for fixed defaults and class methods; reach for the function form when the fallback must be computed each time.
3. Building OOP with Metatables
Lua has no class keyword, yet it has full object-oriented programming — you assemble it from tables and metatables. The recipe: make a table for the class, set Class.__index = Class so instances inherit its methods, and write a constructor (by convention called new) that returns a table whose metatable is the class. Define methods with the colon (function Animal:speak()), which quietly adds a first parameter called self — the instance the method was called on. Inheritance is just one more metatable: point a subclass's __index at its parent.
-- Lua has no 'class' keyword. You build classes from
-- tables + metatables. Setting Class.__index = Class makes
-- instances look up missing keys (their methods) on the class.
local Animal = {}
Animal.__index = Animal -- methods live on Animal; instances inherit them
-- A constructor is just a function that returns a table whose
-- metatable is the class. 'new' is a convention, not a keyword.
function Animal.new(name, sound)
return setmetatable({ name = name, sound = sound }, Animal)
end
-- 'function Animal:speak()' is sugar for 'function Animal.speak(self)'.
-- The colon ':' silently adds a first parameter called 'self'.
function Animal:speak()
return self.name .. " says " .. self.sound .. "!"
end
local cat = Animal.new("Cat", "Meow")
print(cat:speak()) -- prints: Cat says Meow! (cat:speak() passes cat as self)
-- INHERITANCE: make Dog fall back to Animal via __index.
local Dog = setmetatable({}, { __index = Animal }) -- Dog inherits Animal's methods
Dog.__index = Dog -- Dog instances look up on Dog
function Dog.new(name)
local obj = Animal.new(name, "Woof") -- reuse the parent constructor
obj.tricks = {}
return setmetatable(obj, Dog)
end
function Dog:learnTrick(trick)
table.insert(self.tricks, trick) -- 'self' is the Dog instance
end
local rex = Dog.new("Rex")
print(rex:speak()) -- prints: Rex says Woof! (inherited from Animal)
rex:learnTrick("sit")
rex:learnTrick("roll")
print(#rex.tricks) -- prints: 2Common Errors (and the fix)
- Off-by-one from 0-based habits: fruits[0] is nil in Lua — the first element is fruits[1] and the last is fruits[#fruits]. Loop with for i = 1, #t do.
- "attempt to index a nil value (local 'self')": you called a colon-method with a dot, e.g. dog.speak(). Use dog:speak() so self is passed automatically (it equals dog.speak(dog)).
- "attempt to call a nil value (method 'speak')": you forgot Class.__index = Class, so instances can't find their methods. Add that line right after you create the class table.
- Mixing array and hash parts: # only counts the consecutive integer keys. In { "a", color = "red" }, #t is 1 — the color key is invisible to it. Don't rely on # for tables used as dictionaries.
- A nil in the middle of an array: setting t[2] = nil leaves a "hole", and #t may then return 1 or 3 unpredictably. Use table.remove to delete so the rest shift down.
📋 Quick Reference
| Task | Code | Result |
|---|---|---|
| First / last item | t[1] / t[#t] | 1-based |
| Add / remove | table.insert(t, x) / table.remove(t) | end of list |
| Length | #t | item count |
| Attach metatable | setmetatable(t, mt) | returns t |
| Defaults / inheritance | mt.__index = fallback | on missing key |
| Overload + | mt.__add = function(a,b) end | a + b |
| Define method | function T:method() end | self passed in |
| Call method | obj:method() | = obj.method(obj) |
Mini-Challenge: a Counter class
No blanks this time — just a brief and an outline. Build the class yourself, run it, and check your output against the comment. This is the exact shape of objects you'll write in real Lua programs and games.
-- 🎯 MINI-CHALLENGE: a Counter class
-- 1. Make a table Counter and set Counter.__index = Counter
-- 2. Counter.new() returns a new object with { count = 0 }
-- whose metatable is Counter
-- 3. Add a method Counter:increment() that does self.count = self.count + 1
-- 4. Add a method Counter:value() that returns self.count
--
-- Then test it:
-- local c = Counter.new()
-- c:increment()
-- c:increment()
-- print(c:value()) -- ✅ Expected output: 2
-- your code here🎉 Lesson Complete!
- ✅ A table is Lua's only data structure — array and dictionary, indexed from 1
- ✅ table.insert, table.remove, and # manage and measure lists
- ✅ setmetatable attaches behaviour; __index supplies defaults and inheritance
- ✅ __newindex intercepts writes; __add/__eq/__tostring overload operators
- ✅ Classes are tables with Class.__index = Class; :method() passes self
Practice quiz
At what index does a Lua array start?
- 0
- -1
- Any number
- 1
Answer: 1. Lua tables are 1-based; the first element is t[1].
What is Lua's only built-in data structure?
- The table
- The array
- The list
- The struct
Answer: The table. The table serves as both array and dictionary in Lua.
Which function attaches a metatable to a table?
- addmeta(t, mt)
- setmetatable(t, mt)
- meta(t, mt)
- withmeta(t, mt)
Answer: setmetatable(t, mt). setmetatable(t, mt) attaches mt to t and returns t.
Which metamethod supplies a value for a missing key?
- __newindex
- __get
- __index
- __missing
Answer: __index. __index is consulted when a read finds no key on the table.
Which metamethod overloads the + operator?
- __plus
- __sum
- __concat
- __add
Answer: __add. __add defines what a + b does for tables.
What does dog:speak() pass as a hidden first argument?
- self (the dog)
- nil
- the class table
- the method name
Answer: self (the dog). The colon passes the receiver as self automatically.
Which line lets instances find their methods on the class?
- Class.methods = true
- Class.__index = Class
- setmetatable(Class)
- Class.self = Class
Answer: Class.__index = Class. Setting Class.__index = Class makes instances look up methods on the class.
Which metamethod intercepts SETTING a new key?
- __set
- __index
- __newindex
- __write
Answer: __newindex. __newindex runs when you assign to a key the table doesn't already have.
What does the # operator measure on { "a", color = "red" } ?
- 1 (only the array part)
- 2
- 0
- The number of keys
Answer: 1 (only the array part). # counts only the consecutive integer (array) part, ignoring string keys.
Which metamethod customises how a table is printed?
- __print
- __show
- __display
- __tostring
Answer: __tostring. __tostring controls the output of tostring() and print().
Continue this course
- Previous: Control Flow and Loops
- Next: Lua for Game Development — Apply Lua in game engines like LÖVE2D and Roblox with practical examples
Frequently asked questions
Why do Lua arrays start at 1 instead of 0?
It is a deliberate design choice from Lua's authors to read more like everyday counting (the 1st item is at index 1). It catches out programmers coming from C, Java, or Python. The length operator # and table.insert/table.remove all assume 1-based indexing, so embrace it: the first element is t[1] and the last is t[#t].
What is the difference between dog.speak() and dog:speak()?
The colon is shorthand that passes the table itself as a hidden first argument called self. dog:speak() is exactly the same as dog.speak(dog). Define methods with a colon (function Dog:speak()) and call them with a colon (dog:speak()) so self is filled in automatically. Mixing them — calling dog.speak() on a colon-defined method — leaves self as nil and crashes.
What does Class.__index = Class actually do?
It makes the class its own fallback table. When you read a key that an instance does not have (like a method name), Lua follows __index to the class and finds the method there. Without that line, instances cannot see their own methods and you get attempt to call a nil value.
When should __index be a function instead of a table?
Use a table for fixed defaults or class methods (the common case). Use a function — __index = function(t, key) ... end — when the fallback value should be computed on the fly, for example logging every missing-key access, or generating a value lazily the first time it is requested.
Can one table be both an array and a dictionary at the same time?
Yes. A single table has an array part (consecutive integer keys 1, 2, 3...) and a hash part (everything else) living together. { "a", "b", color = "red" } stores "a" at [1], "b" at [2], and "red" at key "color". Just be careful: the # length operator only measures the array part and ignores string keys.