luaguides

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.