Skip to content

Lua Metatables Guide — Operator Overloading and Custom Behavior

DodaTech Updated 2026-06-28 7 min read

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:

  1. rex:make_sound() looks for make_sound in rex -- not found
  2. Checks rex's metatable __index -- points to Dog
  3. Looks for make_sound in Dog -- found! Calls it with self = rex
  4. Inside make_sound, self.sound looks in rex -- not found
  5. Checks rex's metatable __index -- points to Dog
  6. self.sound found in Dog -- 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