Building Classes in Lua with Metatables: OOP Step by Step
Intro context
Building classes in Lua is a question of how you wire up metatables, not a question of language syntax. Lua has no class keyword, but the same metatable mechanism that lets you customise table operations also lets you implement constructors, methods, inheritance, and even private members. This tutorial shows how to assemble those pieces step by step.
In the previous tutorial we explored metatables and metamethods, the mechanism that lets you customise Lua’s behaviour for table operations. Building classes in Lua builds on that foundation: a class is a table acting as a prototype, and the metatable links new instances back to it for method lookup. We’ll cover constructors with new, the self parameter, instance methods, single inheritance, and a pattern for keeping members private. For related reading see the Lua inheritance tutorial and the metatables guide.
From tables to classes
A class in Lua is a table acting as a prototype for creating objects. The metatable links new objects to their class, enabling method lookup and inheritance.
The basic class pattern
Here’s how you create a simple class:
-- Define the class (a table with methods)
local Animal = {}
Animal.__index = Animal
-- Constructor: creates a new Animal instance
function Animal.new(name, species)
local self = setmetatable({}, Animal)
self.name = name
self.species = species
return self
end
-- A method
function Animal:speak()
return "Some generic sound"
end
-- Create an instance
local dog = Animal.new("Buddy", "Dog")
print(dog:speak()) -- Output: Some generic sound
print(dog.name) -- Output: Buddy
The key ingredient is setmetatable({}, Animal). This creates a new empty table and sets its metatable to Animal. When you call dog:speak(), Lua searches for speak in dog, fails, then checks the metatable , finding Animal.__index = Animal, it continues searching there and finds the method.
Understanding the self parameter
In the example above, we used function Animal:speak() instead of function Animal.speak(). The colon syntax is syntactic sugar that automatically passes the object as the first argument (traditionally called self).
-- These are equivalent:
function Animal.speak(self)
return self.name .. " makes a sound"
end
function Animal:speak()
return self.name .. " makes a sound"
end
The colon syntax makes code cleaner and is the standard convention for methods. Always use self when you need to access or modify the object’s properties.
Constructors: multiple approaches
While new() is a common constructor name, you can use any naming convention:
-- Factory function pattern
local Rectangle = {}
Rectangle.__index = Rectangle
function Rectangle.new(width, height)
local self = setmetatable({}, Rectangle)
self.width = width
self.height = height
return self
end
-- Alternative: use the type name itself as constructor
function Rectangle:new(width, height)
local self = setmetatable({}, Rectangle)
self.width = width
self.height = height
return self
end
-- Usage
local rect = Rectangle.new(10, 5)
print(rect.width, rect.height) -- Output: 10 5
Adding more methods
Classes become useful when they have meaningful methods:
function Rectangle:getArea()
return self.width * self.height
end
function Rectangle:getPerimeter()
return 2 * (self.width + self.height)
end
function Rectangle:scale(factor)
self.width = self.width * factor
self.height = self.height * factor
return self -- enable chaining
end
-- Usage
local rect = Rectangle.new(10, 5)
print(rect:getArea()) -- Output: 50
rect:scale(2)
print(rect:getArea()) -- Output: 200
Notice that scale() returns self , this enables method chaining (fluent interface pattern).
Private members in Lua
Lua doesn’t enforce privacy, but you can simulate private members using closures or naming conventions:
Convention-based privacy
local BankAccount = {}
BankAccount.__index = BankAccount
function BankAccount.new(initialBalance)
local self = setmetatable({}, BankAccount)
-- Convention: underscore prefix indicates private
self._balance = initialBalance
return self
end
function BankAccount:deposit(amount)
if amount > 0 then
self._balance = self._balance + amount
return true
end
return false
end
function BankAccount:getBalance()
return self._balance
end
local account = BankAccount.new(100)
account:deposit(50)
print(account:getBalance()) -- Output: 150
-- Direct access still works but convention signals "don't touch"
print(account._balance) -- Output: 150
Closure-based privacy (true privacy)
For stronger privacy, use closures:
function BankAccount.new(initialBalance)
-- Private variable in closure scope
local _balance = initialBalance
local self = setmetatable({}, {__index = BankAccount})
function self:deposit(amount)
if amount > 0 then
_balance = _balance + amount
return true
end
return false
end
function self:getBalance()
return _balance
end
return self
end
local account = BankAccount.new(100)
account:deposit(50)
print(account:getBalance()) -- Output: 150
print(account._balance) -- Output: nil (doesn't exist!)
This approach is more memory-intensive (each object gets its own function closures) but provides true privacy.
Inheritance
Inheritance in Lua is achieved by setting up the prototype chain through metatables:
-- Base class: Animal
local Animal = {}
Animal.__index = Animal
function Animal.new(name, species)
local self = setmetatable({}, Animal)
self.name = name
self.species = species
return self
end
function Animal:speak()
return "Some generic sound"
end
-- Derived class: Dog
local Dog = {}
Dog.__index = Dog
-- Inherit from Animal
setmetatable(Dog, {__index = Animal})
function Dog.new(name)
local self = setmetatable({}, Dog)
self.name = name
self.species = "Dog"
return self
end
-- Override the speak method
function Dog:speak()
return self.name .. " barks!"
end
-- Usage
local animal = Animal.new("Generic", "Animal")
local dog = Dog.new("Buddy")
print(animal:speak()) -- Output: Some generic sound
print(dog:speak()) -- Output: Buddy barks!
print(dog.name) -- Output: Buddy
print(dog.species) -- Output: Dog
When dog:speak() is called, Lua finds speak in Dog. When accessing dog.name, it’s not in Dog, so Lua checks Animal via the metatable chain.
Multi-level inheritance
You can create deeper inheritance hierarchies:
local Bird = {}
Bird.__index = Bird
function Bird:fly()
return self.name .. " is flying"
end
local Parrot = {}
Parrot.__index = Parrot
setmetatable(Parrot, {__index = Bird})
function Parrot:speak()
return self.name .. " says Hello!"
end
local polly = Parrot.new("Polly")
print(polly:fly()) -- Output: Polly is flying
print(polly:speak()) -- Output: Polly says Hello!
Super: calling parent methods
To call a parent method from an overridden method:
function Dog:speak()
-- Call the parent's speak method
local parentSpeech = Animal.speak(self)
return self.name .. " says: " .. parentSpeech
end
Or more elegantly, store a reference:
local Animal = {super = nil}
local Dog = {super = Animal}
setmetatable(Dog, {__index = Animal})
function Dog:speak()
local parentSpeech = Dog.super.speak(self)
return self.name .. " says: " .. parentSpeech
end
Class variables VS instance variables
Sometimes you want class-level variables (shared across all instances):
local Counter = {}
Counter.__index = Counter
Counter.count = 0 -- Class variable
function Counter.new()
local self = setmetatable({}, Counter)
Counter.count = Counter.count + 1
return self
end
function Counter:getCount()
return Counter.count
end
local a = Counter.new()
local b = Counter.new()
print(a:getCount()) -- Output: 2
print(b:getCount()) -- Output: 2
print(Counter.count) -- Output: 2
Summary
Metatables enable object-oriented programming in Lua:
- Classes are tables with methods, indexed via
__index - Constructors create instances using
setmetatable({}, Class) self(the colon syntax) gives methods access to instance data- Private members can use convention (
_name) or closures - Inheritance works by setting up metatable chains
In the next tutorial, we’ll explore more advanced OOP patterns including mixins, multiple inheritance, and static methods.
Ready to practice? Try building a Shape base class with Circle and Square subclasses. Challenge yourself to implement area calculation for each shape type!
Where to go next
Now that you understand building classes in Lua, deepen the pattern by reading the inheritance in Lua tutorial and the Lua closures guide. For broader object-system patterns, see the Lua functional patterns guide.