Lua OOP Guide — Object-Oriented Programming with Prototypes
In this tutorial, you will learn about Lua OOP Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Lua achieves object-oriented programming through Prototype-based inheritance -- using tables as objects, metatables for the prototype chain, and the colon operator for the implicit self parameter.
What You'll Learn
- Creating classes and instances with metatables
- Using colon syntax for methods
- Implementing inheritance through prototype chains
- Adding mixins and multiple inheritance
Why It Matters
Lua doesn't have built-in classes. Instead, it uses a flexible prototype system that lets you build OOP patterns naturally. This approach is simpler than class-based OOP but requires understanding the mechanics. Durga Antivirus Pro uses Lua's prototype OOP for its plugin architecture where each scanner type inherits from a base Scanner prototype.
Real-World Use
A game engine defines Entity and Player prototypes where Player inherits from Entity. A UI library defines Button, TextBox, and Window prototypes that inherit from Widget. Each prototype adds or overrides methods while sharing common behavior.
flowchart LR
A["OOP Prototypes"] --> B["Classes"]
A --> C["Inheritance"]
A --> D["Encapsulation"]
A --> E["Polymorphism"]
B --> F["Instances"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
The Class Pattern
A Lua "class" is a table that acts as a prototype. Instances have their metatable's __index pointing to the class.
-- Define the class (prototype)
local Animal = {}
Animal.__index = Animal
-- Constructor
function Animal:new(name, species)
local instance = {
name = name or "Unknown",
species = species or "Unknown"
}
setmetatable(instance, self)
return instance
end
-- Method
function Animal:speak()
print(self.name .. " makes a sound")
end
-- Method
function Animal:describe()
print(self.name .. " is a " .. self.species)
end
-- Create instances
local rex = Animal:new("Rex", "Dog")
local luna = Animal:new("Luna", "Cat")
rex:speak() --> Rex makes a sound
luna:describe() --> Luna is a Cat
Colon Syntax
The : operator is syntactic sugar that passes the table on the left as the first argument self.
-- These are equivalent:
rex:speak() -- implicit self
rex.speak(rex) -- explicit self
-- So are these:
function Animal:speak() -- implicit self parameter
-- ...
end
function Animal.speak(self) -- explicit self parameter
-- ...
end
Inheritance
Create a subclass by setting its metatable to point to the parent class.
-- Subclass: Dog inherits from Animal
local Dog = {}
setmetatable(Dog, {__index = Animal})
Dog.__index = Dog
-- Override constructor
function Dog:new(name, breed)
local instance = Animal:new(name, "Dog")
instance.breed = breed or "Mixed"
setmetatable(instance, self)
return instance
end
-- Override method
function Dog:speak()
print(self.name .. " says Woof!")
end
-- New method
function Dog:fetch()
print(self.name .. " fetches the ball")
end
-- Usage
local buddy = Dog:new("Buddy", "Golden Retriever")
buddy:speak() --> Buddy says Woof!
buddy:describe() --> Buddy is a Dog (inherited from Animal)
buddy:fetch() --> Buddy fetches the ball
Multiple Inheritance
Lua doesn't have built-in multiple inheritance, but you can implement it with metatables.
-- Mixin tables
local Flyable = {
fly = function(self)
print(self.name .. " is flying")
end
}
local Swimmable = {
swim = function(self)
print(self.name .. " is swimming")
end
}
-- Function to mix in methods from multiple tables
local function mixin(target, ...)
local sources = {...}
for _, source in ipairs(sources) do
for key, value in pairs(source) do
target[key] = value
end
end
return target
end
-- Duck inherits Animal + Flyable + Swimmable
local Duck = {}
setmetatable(Duck, {__index = Animal})
Duck.__index = Duck
mixin(Duck, Flyable, Swimmable)
function Duck:new(name)
local instance = Animal:new(name, "Duck")
setmetatable(instance, self)
return instance
end
function Duck:speak()
print(self.name .. " says Quack!")
end
local donald = Duck:new("Donald")
donald:speak() --> Donald says Quack!
donald:fly() --> Donald is flying
donald:swim() --> Donald is swimming
donald:describe() --> Donald is a Duck
Encapsulation (Private Data)
Lua doesn't have private fields, but you can achieve encapsulation using closures or a naming convention.
-- Using closures for truly private data
local function create_bank_account(initial_balance)
local balance = initial_balance -- private
local account = {}
function account:deposit(amount)
if amount > 0 then
balance = balance + amount
end
return balance
end
function account:withdraw(amount)
if amount > 0 and amount <= balance then
balance = balance - amount
return true
end
return false
end
function account:get_balance()
return balance
end
return account
end
local acc = create_bank_account(1000)
print(acc:get_balance()) --> 1000
acc:deposit(500)
print(acc:get_balance()) --> 1500
print(acc.balance) --> nil (private!)
Polymorphism
Lua's dynamic typing makes polymorphism natural -- any object with the right method can be used interchangeably.
local function animal_sound(animal)
-- Works with any object that has a speak method
animal:speak()
end
local rex = Dog:new("Rex", "Lab")
local donald = Duck:new("Donald")
local luna = Animal:new("Luna", "Cat")
animal_sound(rex) --> Rex says Woof!
animal_sound(donald) --> Donald says Quack!
animal_sound(luna) --> Luna makes a sound
Common Mistakes
1. Forgetting self.__index = self
Without setting __index to itself, the class doesn't inherit its own methods. Instances won't find methods defined on the class.
2. Not calling superclass constructor
When overriding new, always call the parent constructor first to initialize inherited fields.
3. Confusing : with .
Using . instead of : requires explicit self parameter. Mixing them causes "attempt to index a nil value" errors.
4. Modifying class after creating instances
If you change a method on the prototype, existing instances see the change (since __index points to the prototype). This can be intentional or surprising.
5. Using global variable names for class tables
Name your class table with local scope to avoid polluting the global namespace.
Practice Questions
1. How does Lua implement inheritance without classes?
Through metatable __index. When a method isn't found on an instance, Lua looks up the prototype chain via __index.
2. What does the colon operator do?
It passes the table on the left as the first argument (self) to the method. obj:method(args) is equivalent to obj.method(obj, args).
3. How do you create truly private data in Lua? Using closures. Variables local to the constructor function are captured by the returned methods but inaccessible from outside.
Challenge: Create a Shape hierarchy with Circle, Rectangle, and Triangle prototypes. Each should have area() and perimeter() methods. Demonstrate polymorphism by calling these methods on a list of shapes.
FAQ
{{< faq question="Can I use traditional class-based OOP in Lua?" >}}
Lua doesn't have a class keyword, but you can simulate it with metatables. Libraries like middleclass and 30log provide class syntax if you prefer it.
{{< /faq >}}
{{< faq question="What is the difference between prototype-based and class-based OOP?" >}} In class-based OOP (Java, C++), classes are blueprints and instances are created from them. In prototype-based OOP (Lua, JavaScript), objects are created from existing objects (prototypes) and can be extended individually. {{< /faq >}}
{{< faq question="Can I call the parent class method from an override?" >}}
Yes. Store the parent method and call it: Dog.speak = function(self) Animal.speak(self); print(self.name .. " also barks") end.
{{< /faq >}}
{{< faq question="Is there a standard way to define classes in Lua?" >}}
No single standard, but the pattern shown here (class table with __index = self, :new() constructor) is the most widely used. Libraries offer syntactic sugar but the underlying mechanism is always metatables.
{{< /faq >}}
{{< faq question="Can I add methods to a single instance?" >}}
Yes. Since each instance is its own table, you can add methods directly: rex.jump = function(self) print(self.name .. " jumps") end. This won't affect other instances.
{{< /faq >}}
Try It Yourself
local Vehicle = {}
Vehicle.__index = Vehicle
function Vehicle:new(make, model, year)
local inst = {make = make, model = model, year = year, speed = 0}
setmetatable(inst, self)
return inst
end
function Vehicle:accelerate(amount)
self.speed = self.speed + (amount or 10)
print(self.model .. " accelerated to " .. self.speed .. " mph")
end
function Vehicle:describe()
print(self.year .. " " .. self.make .. " " .. self.model)
end
local Car = {}
setmetatable(Car, {__index = Vehicle})
Car.__index = Car
function Car:new(make, model, year, doors)
local inst = Vehicle:new(make, model, year)
inst.doors = doors or 4
setmetatable(inst, self)
return inst
end
function Car:honk()
print(self.model .. " honks: Beep beep!")
end
local civic = Car:new("Honda", "Civic", 2024, 4)
civic:describe()
civic:honk()
civic:accelerate(20)
Expected output:
2024 Honda Civic
Civic honks: Beep beep!
Civic accelerated to 20 mph
What's Next
Now that you understand OOP with prototypes, learn about coroutines -- Lua's cooperative multitasking mechanism.
| Topic | Description | Link |
|---|---|---|
| Coroutines | Cooperative multitasking and yield | {{< ref "08-coroutines" >}} |
| File I/O | Reading and writing files | {{< ref "09-file-io" >}} |
| Python OOP | Compare Python and Lua OOP approaches | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro