{}const=>[]async()letfn</>var
LuaDevelopment

Metatables in Lua: the simple about the complex

Let's take a look at what metatables are in Lua, why they are needed, and how to use them to add "superpowers" to tables.

К

Kodik

Author

4 min read

We often want tables to behave “in a special way”: to add up like vectors, to substitute default values, to print nicely. ✨ For this, Lua has metatables - hidden scriptwriters of table behavior. Below is a brief, practical and painless analysis.

🧩 What are metatables

Metatable — is a regular table attached to another table via setmetatable(), which describes how the object reacts to operations: indexing, addition, comparison, concatenation, call, etc.

Think of the metatable as a “layer of rules”: Lua checks the keys and operations — and, if it finds a corresponding metamethod (for example, __index), it uses it.

🧰 Why is it necessary

Task

Metamethod

Default values / prototype

__index

Control/prohibition of recording

__newindex

Override addition/multiplication/...

__add, __mul, …

Beautiful output

__tostring

Comparison / length / concatenation

__eq, __lt, __len, __concat

Making a "called" table

__call

Protecting the metatable

__metatable

🔥 100,000+ students already with us

Tired of reading theory?
Time to code!

Kodik — an app where you learn to code through practice. AI mentor, interactive lessons, real projects.

🤖 AI 24/7
🎓 Certificates
💰 Free
🚀 Start learning
Joined today

The shortest example: adding tables as vectors

local mt = {
  __add = function(a, b)
    return { a[1] + b[1], a[2] + b[2] }
  end
}

local v1 = setmetatable({1, 2}, mt)
local v2 = setmetatable({3, 4}, mt)

local sum = v1 + v2
print(sum[1], sum[2]) --> 4  6

Without metatables, v1 + v2 would cause an error: Lua does not know how to add tables.

🧭 __index: default values and "inheritance"

__index is triggered when there is no key in the table. It can be set by a reference to a prototype table or a function.

Option 1: prototype

local defaults = { speed = 10, hp = 100 }
local player = setmetatable({}, { __index = defaults })

print(player.speed)  --> 10 (берётся из defaults)
player.speed = 20    -- теперь свой speed у player
print(player.speed)  --> 20

Option 2: auto-create values (lazy initialization)

local counts = setmetatable({}, {
  __index = function(t, key)
    local v = 0
    rawset(t, key, v)     -- записываем сразу, чтобы дальше было «как будто существовало всегда»
    return v
  end
})

counts["apples"] = counts["apples"] + 1
print(counts["apples"]) --> 1

We use rawset to avoid calling __newindex and getting into recursion.

🛡️ __newindex: record control

local guarded = setmetatable({ x = 0, y = 0 }, {
  __newindex = function(t, k, v)
    if k == "x" or k == "y" then
      rawset(t, k, v)              -- безопасная запись без повторного вызова __newindex
    else
      error("You cannot add a new field: " .. tostring(k), 2)
    end
  end
})

guarded.x = 10    -- ок
guarded.title = "oops"  -- ошибка

A common mistake is to write t[k] = v inside __newindex. This will again call __newindex and lead to recursion. Use rawset.

⚠️ Pitfalls

  • Metatables are not copied when surface copying a table, re-hang it.

  • Too tricky chains __index complicate debugging. Keep the model simple.

  • rawget/rawset bypass metamethods — this is both a strength and a risk. Use them consciously.

🚀 Advanced Metatables Techniques

📞 __call: making a "factory" of objects

The __call metamethod allows you to call a table as a function — convenient for factories and DSL.

local greeter = setmetatable({}, {
  __call = function(_, name) print("Hi, " .. name .. "!") end
})

greeter("Code") --> Привет, Кодик!

🔒 __metatable: protection against changes

You can hide real metamethods and prohibit them from changing as follows:

local obj = {}
local mt  = { __metatable = "Access to the metatable is denied" }
setmetatable(obj, mt)

print(getmetatable(obj)) --> Доступ к метатаблице запрещён
-- getmetatable(obj).__index  -- уже не получить

🎨 Beautiful output and "magic" operators

local mt = {
  __tostring = function(t) return ("Point(%d, %d)"):format(t.x, t.y) end,
  __len      = function(t) return math.sqrt(t.x*t.x + t.y*t.y) end, -- длина вектора через #
  __eq       = function(a,b) return a.x==b.x and a.y==b.y end,
  __add      = function(a,b) return setmetatable({x=a.x+b.x, y=a.y+b.y}, mt) end,
}
local p = setmetatable({x=3, y=4}, mt)
print(p)    --> Point(3, 4)
print(#p)   --> 5

💬 Where would you use metatables in your game on Roblox/Love2D, in the config system, or in mini-DSL?

📚 Do you want to delve into the topic?

In the attachment Code you will find detailed Lua lessons, step-by-step exercises, error analysis and convenient practice right on your phone or browser.

And if you want to be aware of the news, new features and useful materials - subscribe to our Telegram channel. It's cozy, businesslike and with love for code ❤️

🎯Stop procrastinating

Liked the article?
Time to practice!

In Kodik, you don't just read — you write code immediately. Theory + practice = real skills.

Instant practice
🧠AI explains code
🏆Certificate

No registration • No card