Lua Metatables Guide — Operator Overloading and Custom Behavior
In this tutorial, you will learn about Lua Metatables Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Lua metatables are special tables that define custom behavior for other tables -- they control how tables respond to operators (+, -, []), method lookup, and garbage collection through named metamethods.
What You'll Learn
- What metatables are and how to set them
- Common metamethods: __add, __index, __newindex, __tostring, __call
- Using __index for prototypal inheritance
- Creating custom vector and matrix types
Why It Matters
Metatables let you customize Lua's behavior at a fundamental level. They are how Lua implements operator overloading, inheritance, default values, and lazy properties. Durga Antivirus Pro uses metatables for its sandbox environment to intercept and restrict dangerous operations.
Real-World Use
A 2D game engine uses metatables to implement vector math with natural + and * operators. A configuration library uses metatables to provide default values for missing keys. An ORM uses metatables to intercept field access and trigger lazy loading from a database.
flowchart LR
A["Metatables"] --> B["__index"]
A --> C["__add"]
A --> D["__tostring"]
A --> E["__call"]
B --> F["Inheritance"]
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
What is a Metatable?
Every table can have a metatable -- another table that controls its behavior. Set it with setmetatable and read it with getmetatable.
local t = {}
local mt = {
__tostring = function() return "custom table" end
}
setmetatable(t, mt)
print(t) --> custom table
Metamethods are named entries in the metatable that define behavior for specific operations. They always start with double underscores.
__tostring -- Custom String Representation
Controls how a table appears when converted to a string (via print, tostring, or string concatenation).
local point = {x = 3, y = 4}
local mt = {
__tostring = function(t)
return "Point(" .. t.x .. ", " .. t.y .. ")"
end
}
setmetatable(point, mt)
print(point) --> Point(3, 4)
__add and Arithmetic Metamethods
-- Vector class with addition
local Vector = {}
Vector.__index = Vector
function Vector:new(x, y)
return setmetatable({x = x, y = y}, self)
end
function Vector.__add(a, b)
return Vector:new(a.x + b.x, a.y + b.y)
end
function Vector.__sub(a, b)
return Vector:new(a.x - b.x, a.y - b.y)
end
function Vector.__mul(a, b)
if type(a) == "number" then
return Vector:new(a * b.x, a * b.y)
end
return Vector:new(a.x * b, a.y * b)
end
function Vector.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local v1 = Vector:new(3, 4)
local v2 = Vector:new(1, 2)
print(v1 + v2) --> (4, 6)
print(v1 - v2) --> (2, 2)
print(v1 * 2) --> (6, 8)
Full list of arithmetic metamethods: __add, __sub, __mul, __div, __idiv (floor division), __mod, __pow, __unm (unary minus), __band (bitwise AND), __bor, __bxor, __bnot, __shl, __shr, __concat.
__index -- The Key to Inheritance
__index is the most important metamethod. It's called when a table doesn't have a requested key. This is Lua's inheritance mechanism.
-- Base class
local Animal = {
sound = "generic sound"
}
function Animal:make_sound()
print(self.name .. " says " .. self.sound)
end
function Animal:new(name)
local obj = {name = name}
setmetatable(obj, self)
self.__index = self
return obj
end
-- Subclass using __index inheritance
local Dog = Animal:new("Dog placeholder")
Dog.sound = "Woof!"
local Cat = Animal:new("Cat placeholder")
Cat.sound = "Meow!"
local rex = Dog:new("Rex")
local luna = Cat:new("Luna")
rex:make_sound() --> Rex says Woof!
luna:make_sound() --> Luna says Meow!
How __index works:
rex:make_sound()looks formake_soundinrex-- not found- Checks
rex's metatable__index-- points toDog - Looks for
make_soundinDog-- found! Calls it withself = rex - Inside
make_sound,self.soundlooks inrex-- not found - Checks
rex's metatable__index-- points toDog self.soundfound inDog-- returns "Woof!"
__index as a Function
__index can also be a function, providing dynamic lookup:
local defaults = setmetatable({}, {
__index = function(t, key)
if type(key) == "string" then
return "default_" .. key
end
return nil
end
})
print(defaults.name) --> default_name
print(defaults.color) --> default_color
__newindex -- Intercepting Writes
__newindex is called when an attempt is made to set a key that doesn't exist.
local protected = {}
local mt = {
__newindex = function(t, key, value)
if key == "admin" then
error("Cannot set admin property")
else
rawset(t, key, value)
end
end
}
setmetatable(protected, mt)
protected.name = "Alice" --> OK
protected.admin = true --> ERROR: Cannot set admin property
Combining __index and __newindex for Read-Only Tables
local function read_only(t)
local proxy = {}
local mt = {
__index = t,
__newindex = function()
error("Table is read-only")
end
}
setmetatable(proxy, mt)
return proxy
end
local data = {name = "secret", key = 12345}
local safe = read_only(data)
print(safe.name) --> secret
safe.name = "new" --> ERROR: Table is read-only
__call -- Making Tables Callable
When you put () after a table, Lua checks for __call in its metatable.
local function create_counter()
local t = {count = 0}
setmetatable(t, {
__call = function(self, increment)
self.count = self.count + (increment or 1)
return self.count
end
})
return t
end
local counter = create_counter()
print(counter()) --> 1
print(counter(5)) --> 6
print(counter()) --> 7
print(counter.count) --> 7
__mode -- Weak Tables
__mode controls whether references in a table are weak (ignored by the garbage collector).
-- Keys are weak
local weak_keys = setmetatable({}, {__mode = "k"})
-- Values are weak
local weak_vals = setmetatable({}, {__mode = "v"})
-- Both keys and values are weak
local weak_both = setmetatable({}, {__mode = "kv"})
Use weak tables for caches and association tables that shouldn't prevent garbage collection.
Common Mistakes
1. Forgetting to set __index to self
When creating class hierarchies, always set self.__index = self in your constructor. Without it, __index defaults to nil and method lookup fails.
2. Using rawget/rawset without understanding the tradeoff
rawget and rawset bypass metatable behavior. Use them inside __index and __newindex to avoid infinite Recursion, but avoid them elsewhere.
3. Setting metatable on the wrong thing
Set the metatable on instances, not on the class itself (unless you want class-level metamethods). The pattern is: instances have metatable pointing to the class table.
4. Expecting __index to intercept existing keys
__index only fires when the key doesn't exist in the table. For existing keys, the value is returned directly.
5. Circular metatables causing infinite loops
If __index returns a table that doesn't have the key and has __index pointing back, you get infinite recursion.
Practice Questions
1. What is a metatable? A metatable is a table that defines custom behavior for another table through named metamethods (__add, __index, etc.).
2. How does __index enable inheritance?
When a key isn't found in a table, Lua looks up the metatable's __index field. If it's a table, Lua looks in that table. This creates a Prototype chain.
3. What is the difference between __index and __newindex?
__index fires when reading a missing key. __newindex fires when writing a missing key. They control read and write behavior respectively.
Challenge: Create a Matrix class using metatables that supports addition, subtraction, multiplication, and string representation for 2x2 matrices.
FAQ
{{< faq question="Can I change a table's metatable after creation?" >}}
Yes. Use setmetatable(table, new_mt) to change or update the metatable at any time. Use getmetatable(table) to read it.
{{< /faq >}}
{{< faq question="What happens if __index is a function?" >}}
The function receives the table and the key: __index = function(t, key) ... end. It should return the value for that key. This is useful for computed or dynamic properties.
{{< /faq >}}
{{< faq question="Can I have multiple levels of inheritance?" >}}
Yes. If __index points to a table that has its own __index, Lua follows the chain until it finds the key or reaches the end. This creates arbitrary-depth prototype chains.
{{< /faq >}}
{{< faq question="What is rawget and rawset?" >}}
rawget(t, key) and rawset(t, key, value) bypass the metatable entirely. They access or modify the table directly without triggering __index or __newindex. Use them inside metamethods to avoid infinite recursion.
{{< /faq >}}
{{< faq question="Can I use metatables on strings or numbers?" >}} String metatables are available (the metatable for all strings). Numbers and booleans don't have metatables. You cannot set individual metatables on primitive values. {{< /faq >}}
Try It Yourself
-- A simple readonly wrapper
local function protect(t)
return setmetatable({}, {
__index = t,
__newindex = function(t, k, v)
error("Cannot modify protected table")
end,
__tostring = function(t)
local parts = {}
for k, v in pairs(t) do
table.insert(parts, k .. "=" .. tostring(v))
end
return "{" .. table.concat(parts, ", ") .. "}"
end
})
end
local original = {name = "Alice", age = 30}
local protected = protect(original)
print(protected)
print("Name:", protected.name)
-- protected.name = "Bob" -- would error
Expected output:
{age=30, name=Alice}
Name: Alice
What's Next
Now that you understand metatables, learn how to build full object-oriented programs using Lua's prototype-based inheritance.
| Topic | Description | Link |
|---|---|---|
| OOP | Prototype-based object orientation | {{< ref "07-oop-prototypes" >}} |
| Coroutines | Cooperative multitasking | {{< ref "08-coroutines" >}} |
| JavaScript | Compare with JS prototypal inheritance | JavaScript |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro