Metatables & Object-Oriented Lua
Learn setmetatable and getmetatable, the __index metamethod for inheritance, operator overloading metamethods, and how to build a simple class system.
Introduction
Lua has no class keyword, no built-in inheritance syntax, and no operator overloading syntax — and yet real Lua codebases use all three constantly. The mechanism behind every one of them is the metatable: a special table you can attach to another table to change how it behaves.
- How to attach and inspect a metatable with setmetatable and getmetatable.
- How the __index metamethod enables inheritance-like behavior.
- How to build a simple, working class system out of tables and metatables.
- How metamethods like __add and __tostring let a table respond to operators and printing.
setmetatable and getmetatable
Use case: setmetatable(table, metatable) attaches a metatable to a table, and getmetatable(table) retrieves it. The metatable itself is just an ordinary table whose specially-named fields (like __index, described next) Lua checks automatically during certain operations.
local t = {}local mt = {}
setmetatable(t, mt)print(getmetatable(t) == mt) -- trueClick Run to see what this code prints.
The __index Metamethod
Use case: when you look up a key that doesn't exist directly on a table, Lua checks that table's metatable for an __index field. If __index is itself a table, Lua looks the key up there instead — the exact mechanism that implements inheritance-like "fallback" lookup.
local defaults = {species = "Unknown", legs = 4}
local dog = {species = "Dog"}setmetatable(dog, {__index = defaults})
print(dog.species) -- "Dog" -- found directly on dogprint(dog.legs) -- 4 -- not on dog, falls back to defaults via __indexClick Run to see what this code prints.
Building a Simple Class System
Combining __index (falling back to a shared table of methods), the colon syntax from lesson 7, and a constructor function gives you a complete, working class system — no special syntax required.
local Animal = {}Animal.__index = Animal -- Animal itself acts as the "class" of methods
function Animal.new(name, sound) local self = setmetatable({}, Animal) self.name = name self.sound = sound return selfend
function Animal:speak() print(self.name .. " says " .. self.sound)end
local dog = Animal.new("Rex", "Woof")local cat = Animal.new("Whiskers", "Meow")
dog:speak()cat:speak()Click Run to see what this code prints.
Animal.new creates an empty table and sets its metatable to Animal itself, with __index = Animal. When you call dog:speak(), Lua looks for speak on dog, doesn't find it, checks dog's metatable's __index (which is Animal), finds speak there, and calls it with dog as self — the exact same fallback mechanism as the earlier defaults example, just used to simulate a class.
Operator Overloading Metamethods
Metatables can also define how a table responds to operators like +, -, ==, and <, by setting specially-named metamethod fields.
local Vector = {}Vector.__index = Vector
function Vector.new(x, y) return setmetatable({x = x, y = y}, Vector)end
Vector.__add = function(a, b) return Vector.new(a.x + b.x, a.y + b.y)end
local v1 = Vector.new(1, 2)local v2 = Vector.new(3, 4)local v3 = v1 + v2 -- calls Vector.__add automatically
print(v3.x, v3.y)Click Run to see what this code prints.
| Metamethod | Triggered By |
|---|---|
| __add | a + b |
| __sub | a - b |
| __eq | a == b |
| __lt | a < b |
| __len | #a |
| __call | a(...) — calling a table like a function |
__tostring for Readable Output
Without __tostring, printing a table shows an unhelpful memory address like table: 0x55b1a2c3d4e0. Defining __tostring gives you full control over how a value looks when passed to print() or tostring().
Vector.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")"end
print(v3) -- now calls __tostring automaticallyClick Run to see what this code prints.
Common Mistakes
- Forgetting Class.__index = Class in a constructor pattern — without it, __index has no value and method lookup silently fails.
- Assuming metatables are copied when a table is copied — assigning a table to a new variable does not clone the table or its metatable, both still point to the same underlying table.
- Overusing operator overloading for types where +/- don't have an obvious, intuitive meaning — it can make code harder to read, not easier.
Best Practices
- Follow the Class.new() + Class.__index = Class pattern consistently — it's the de facto standard across the Lua ecosystem.
- Define __tostring on any type you'll frequently print or log, for far easier debugging.
- Reserve operator overloading (__add, __eq, and similar) for types where the operator's meaning is genuinely obvious, like vectors or money.
Frequently Asked Questions
Yes — by chaining __index metatables (a Dog's metatable's __index points to Animal, whose own __index could point to a further base class), you can build multi-level inheritance hierarchies the same way.
It's exactly what real-world Lua object-oriented code looks like — several small libraries exist purely to reduce the boilerplate of this exact pattern, but the underlying mechanism is always __index and setmetatable.
The counterpart to __index for writes — it lets you intercept assignment to a missing key on a table, commonly used to make read-only tables or to validate values before they're stored.
Key Takeaways
- A metatable is an ordinary table attached to another table with setmetatable, whose special fields Lua checks automatically.
- __index enables fallback lookup, which is the mechanism behind both defaults and class-style inheritance.
- The Class.new() / Class.__index = Class pattern is Lua's de facto standard for object-oriented code.
- Metamethods like __add, __eq, and __tostring let a table respond to operators and printing.
Summary
Metatables are how Lua stays small while still supporting real object-oriented patterns and operator overloading — everything is built from the same table and function mechanisms you already know. Next, you'll cover string manipulation and Lua's own lightweight pattern-matching syntax.