OOP with Metatables
Reviewed & published by Brayan K
By the end of this lesson you'll build full object-oriented code in Lua without a class keyword — using a class table, __index for method lookup, a Class.new(...) constructor with setmetatable, the colon : method syntax and self, and single inheritance by chaining one class's __index to another.
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
- Build a class from a plain table plus a metatable
- Use __index so instances find methods on the class
- Write a Class.new(...) constructor with setmetatable
- Define and call methods with the colon : and understand self
- Tell obj:method() apart from obj.method(self)
- Implement single inheritance by chaining __index to a parent
💡 Real-World Analogy
A class is a cookie cutter and each instance is a cookie it stamps out. The cutter (the class table) holds the shared shape — the methods every cookie should have — while each cookie carries its own dough: its own data fields. The __index link is the rule "if a cookie doesn't carry something itself, ask the cutter." Because Lua gives you tables and metatables, you don't need a special class machine: you wire up the cutter, the cookies, and that one fallback rule yourself, and full OOP falls out.
1. A Class Is Just a Table
Lua ships no class keyword, so you build one from parts you already know. Make a table for the class, set Class.__index = Class so instances can find their methods on it, and write a constructor — conventionally named new — that creates a fresh table, attaches the class as its metatable with setmetatable, fills in the instance's data, and returns it. Methods defined with a colon receive a hidden first argument, self, which is the instance. Read and run this complete little class.
-- Lua has no 'class' keyword. A class is just a table, and
-- setting Class.__index = Class makes instances look up their
-- methods on that class when the key is missing on the instance.
local Account = {}
Account.__index = Account -- the key line: methods live on Account
-- A constructor is an ordinary function. 'new' is a convention.
function Account.new(owner, balance)
local self = setmetatable({}, Account) -- new table, metatable = Account
self.owner = owner
self.balance = balance or 0
return self
end
-- Methods defined with a COLON get a hidden first parameter 'self'.
function Account:deposit(amount)
self.balance = self.balance + amount -- 'self' is the instance
return self.balance
end
function Account:describe()
return self.owner .. " has $" .. self.balance
end
-- Create instances and call methods with a colon.
local a = Account.new("Ada", 100)
print(a:deposit(50)) -- prints: 150
print(a:describe()) -- prints: Ada has $1502. The Colon, self, and Why They Matter
The colon is the one piece of pure syntax sugar in Lua OOP. Writing function Dog:bark() is identical to function Dog.bark(self), and calling rex:bark() is identical to rex.bark(rex) — the colon just adds or supplies that first self argument for you. self is the receiver: the specific object the method was called on, so the same shared method can operate on many different instances. The classic mistake is calling a colon-method with a dot, which forgets self and leaves it nil.
-- The colon ':' is pure SUGAR. These two definitions are identical:
local Dog = {}
Dog.__index = Dog
function Dog.new(name) return setmetatable({ name = name }, Dog) end
-- Colon form — Lua adds 'self' for you:
function Dog:bark() return self.name .. " says woof" end
-- Dot form — you must list 'self' yourself:
-- function Dog.bark(self) return self.name .. " says woof" end
local rex = Dog.new("Rex")
-- Calling with a colon passes the receiver as self automatically:
print(rex:bark()) -- prints: Rex says woof
-- ...which is exactly the same as the dot call where you pass it:
print(rex.bark(rex)) -- prints: Rex says woof
-- ⚠️ Calling a colon-method with a DOT forgets self -> it becomes nil:
-- print(rex.bark()) -- error: attempt to index a nil value (local 'self')Your turn. Finish the Circle class: wire up the metamethod, set the metatable in the constructor, and define area as a colon method. Fill in the blanks marked ___.
-- 🎯 YOUR TURN — finish the Circle class, then run it.
local Circle = {}
Circle.___ = Circle -- 👉 the metamethod that enables method lookup
function Circle.new(radius)
-- 1) make a new table whose metatable is Circle
local self = setmetatable({}, ___) -- 👉 the class table
self.radius = radius
return self
end
-- 2) area() uses self.radius. Define it so 'self' is passed automatically:
function Circle:area() -- 👉 a colon makes 'self' appear
return 3.14159 * self.radius * self.radius
end
local c = Circle.new(2)
print(string.format("%.2f", c:area()))
-- ✅ Expected output:
-- 12.573. Single Inheritance by Chaining __index
Inheritance is one more metatable. A subclass like Dog gets its own metatable whose __index points at the parent Animal, while Dog.__index = Dog keeps instance lookups hitting Dog first. The result is a lookup chain: Lua checks the instance, then the subclass, then the parent, stopping at the first match. The child constructor typically calls the parent constructor to reuse its setup, then re-points the new object's metatable to the child so the child's own methods win.
-- INHERITANCE: make a child class fall back to a parent.
-- The trick is a SECOND metatable: the child's __index chain points
-- to the parent so missing methods are found one level up.
local Animal = {}
Animal.__index = Animal
function Animal.new(name, sound)
return setmetatable({ name = name, sound = sound }, Animal)
end
function Animal:speak()
return self.name .. " says " .. self.sound
end
-- Dog INHERITS from Animal: set Dog's metatable so its __index = Animal.
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog -- Dog instances still look up on Dog first
function Dog.new(name)
local self = Animal.new(name, "Woof") -- reuse the parent constructor
return setmetatable(self, Dog) -- but re-point the metatable to Dog
end
-- A method only Dogs have:
function Dog:fetch()
return self.name .. " fetches the ball"
end
local rex = Dog.new("Rex")
print(rex:speak()) -- prints: Rex says Woof (inherited from Animal)
print(rex:fetch()) -- prints: Rex fetches the ball (Dog's own method)Now you try working with self. Finish the Counter so increment updates the instance's own field and value reads it back. Fill in the two blanks:
-- 🎯 YOUR TURN — a Counter object that remembers its count.
local Counter = {}
Counter.__index = Counter
function Counter.new()
return setmetatable({ count = 0 }, Counter)
end
-- 1) increment() adds 1 to the instance's own count
function Counter:increment()
self.count = self.___ + 1 -- 👉 the field that stores the running total
end
-- 2) value() returns the current count
function Counter:value()
return ___.count -- 👉 the receiver passed into the method
end
local c = Counter.new()
c:increment()
c:increment()
c:increment()
print(c:value()) -- ✅ Expected output: 3Pro Tips
- 💡 Always pair the colon. Define with function T:m() and call with obj:m() so self is consistent on both sides.
- 💡 Class.__index = Class right after the table. Put it on the line below local Class = so you never forget it.
- 💡 Reuse the parent constructor. In a child's new, call Parent.new(...) then setmetatable the result to the child instead of duplicating field setup.
- 💡 Shared methods are cheap. Because methods live once on the class, creating thousands of instances costs only their own data, not copies of the functions.
Common Errors (and the fix)
- "attempt to call a nil value (method 'speak')": you forgot Class.__index = Class, so instances can't reach their methods. Fix: add that line right after creating the class table.
- "attempt to index a nil value (local 'self')": you called a colon-method with a dot, e.g. rex.bark(). Fix: call it with a colon, rex:bark(), so self is passed.
- Inherited methods not found: you set Dog.__index = Dog but never linked Dog to Animal. Fix: also do setmetatable(Dog, { __index = Animal }) so the chain reaches the parent.
- All instances share the same data: you put fields on the class table instead of inside new. Fix: create per-instance fields on the self table you return from the constructor, not on Class directly.
📋 Quick Reference
| Task | Code | Result / Note |
|---|---|---|
| Make a class | table + method lookup | |
| Constructor | new instance | |
| Define method | function C:m() ... end | self passed in |
| Call method | obj:m() | = obj.m(obj) |
| Per-instance data | self.field = value | inside new/methods |
| Inherit from parent | setmetatable(C, { __index = P }) | chains lookup |
Mini-Challenge: a Stack class
No blanks this time — just a brief and an outline. You'll combine a class table, a constructor with setmetatable, and colon methods that work on self.items. Build it, run it, and check your output against the example.
-- 🎯 MINI-CHALLENGE: a Stack class
-- 1. Make a table Stack and set Stack.__index = Stack
-- 2. Stack.new() returns a new object { items = {} } with metatable Stack
-- 3. Stack:push(v) -> table.insert(self.items, v)
-- 4. Stack:pop() -> return table.remove(self.items) (last item)
-- 5. Stack:size() -> return #self.items
--
-- ✅ Example run:
-- local s = Stack.new()
-- s:push(10)
-- s:push(20)
-- print(s:size()) -- 2
-- print(s:pop()) -- 20
-- print(s:size()) -- 1
-- your code here🎉 Lesson Complete!
- ✅ A class is a plain table; Class.__index = Class lets instances find its methods
- ✅ A constructor (new) builds a table and calls setmetatable(obj, Class)
- ✅ The colon : defines and calls methods, passing the instance as self
- ✅ obj:method() equals obj.method(obj); calling with a dot loses self
- ✅ Single inheritance chains a subclass's __index to its parent class
Practice quiz
Does Lua have a built-in 'class' keyword?
- No, classes are built from tables and metatables
- Only in Lua 5.4
- Only with an external library
- Yes, like Java
Answer: No, classes are built from tables and metatables. Lua has no class keyword; you assemble OOP from plain tables plus metatables.
Which metamethod makes instances find methods defined on the class?
- __call
- __lookup
- __index
- __methods
Answer: __index. When a key is missing on the instance, Lua follows __index to the class to find the method.
What is the conventional line that lets a class hold its own methods as fallbacks?
- setmetatable(Class)
- Class.__index = Class
- Class.self = Class
- Class.proto = Class
Answer: Class.__index = Class. Setting Class.__index = Class makes the class its own method table for its instances.
What does the colon in obj:method() do?
- Looks up the metatable
- Nothing special
- Calls a static method
- Passes obj as a hidden first argument named self
Answer: Passes obj as a hidden first argument named self. obj:method() is sugar for obj.method(obj); the colon supplies self automatically.
Inside a method defined with a colon, what does 'self' refer to?
- The instance the method was called on
- The class table
- The global environment
- The metatable
Answer: The instance the method was called on. self is the receiver — the specific object on which the method was invoked.
What does a constructor like Class.new(...) typically return?
- The class table itself
- A new table with its metatable set to the class
- A number
- nil
Answer: A new table with its metatable set to the class. new creates a fresh table and calls setmetatable(obj, Class) before returning it.
Which function actually links an instance to its class?
- rawset
- newproxy
- getmetatable
- setmetatable
Answer: setmetatable. setmetatable(obj, Class) attaches the class as the object's metatable.
How is single inheritance commonly implemented?
- By copying every method into the child
- With a special inherits keyword
- By chaining the child class's __index to the parent
- By calling super()
Answer: By chaining the child class's __index to the parent. You set the subclass's metatable so its __index points at the parent, chaining the lookup.
If you call a colon-defined method with a dot (obj.method()), what happens?
- It calls the parent method
- self is nil and indexing it errors
- It works the same way
- Lua passes the class instead
Answer: self is nil and indexing it errors. Without the colon, self is never passed, so it is nil and the first self.field access fails.
Why prefer Class.__index = Class over copying methods into each instance?
- Methods are shared, saving memory and allowing later edits
- It looks shorter only
- It is required by Lua syntax
- It makes instances immutable
Answer: Methods are shared, saving memory and allowing later edits. Sharing one method table means every instance reuses the same functions instead of duplicating them.
Continue this course
- Previous: Strings & Pattern Matching
- Next: Coroutines — Cooperative multitasking with create, resume, yield and wrap
Frequently asked questions
Why do I have to write Class.__index = Class?
Because Lua finds an instance's methods through the __index metamethod. When you write obj:speak() and obj has no speak field of its own, Lua looks at obj's metatable and follows its __index to find the method. By setting Class.__index = Class you make the class its own method table, so every instance whose metatable is Class can see all the methods you define on Class. Omit that line and you get 'attempt to call a nil value (method speak)' because instances cannot reach their methods.
What is the difference between obj.method() and obj:method()?
The colon is shorthand that passes the object itself as a hidden first argument called self. obj:method() is exactly obj.method(obj). You normally define methods with a colon (function Dog:bark()) and call them with a colon (rex:bark()) so self is filled in automatically. The classic bug is calling a colon-defined method with a dot — rex.bark() — which leaves self as nil, so the first self.field access crashes. Keep colons on both the definition and the call.
What exactly is self?
self is the receiver: the specific instance the method was called on. When you write rex:bark(), Lua passes rex as self, so inside bark, self.name reads rex's name. Different objects calling the same shared method each get their own self, which is how one set of methods on the class can operate on many distinct instances. self is not a keyword — it is just the conventional name the colon syntax gives that first parameter.
How does single inheritance work without a 'class' keyword?
You chain the __index lookups. A subclass like Dog gets its own metatable whose __index points at the parent Animal (setmetatable(Dog, { __index = Animal })), while Dog.__index = Dog keeps instance lookups going to Dog first. So when an instance calls a method, Lua checks the instance, then Dog, then Animal, walking up the chain until it finds the method or reaches the end. The child constructor usually calls the parent constructor to reuse its setup, then re-points the metatable to the child.
Why share methods through __index instead of copying them into every object?
Memory and maintainability. If you copied every method into each new instance, a thousand objects would hold a thousand duplicate copies of the same functions. With Class.__index = Class there is exactly one shared method table, and every instance reuses it. As a bonus, if you add or fix a method on the class later, all existing instances immediately see the change because they look it up dynamically rather than holding a frozen copy.