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

💡 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 $150

2. 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.57

3. 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: 3

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeResult / Note
Make a classtable + method lookup
Constructornew instance
Define methodfunction C:m() ... endself passed in
Call methodobj:m()= obj.m(obj)
Per-instance dataself.field = valueinside new/methods
Inherit from parentsetmetatable(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!

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

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.