When you come to Lua after Python or JS and learn that there are no arrays, no objects, no dictionaries... There are only tables.
Your face: 😐
My face a week later, when I realized the power of the tables: 🤯
Let's figure out why tables in Lua are not a "poverty of language" but an ingenious design that beginners stubbornly underestimate.

Wait, are there really only tables here?
Yes. One data structure for all occasions. You want an array? A table. A dictionary? Table. An object with methods? You guessed it — a table. A queue, stack, graph, tree? All tables.
Lua developer explaining the project architecture:
— And here we have a table of tables that stores tables with tables.
- I see, thanks.
It sounds crazy, but in practice it is incredibly elegant.
Table as an array: indexing from one (yes, from ONE)
Here's the first shock for anyone who came from the world of normal languages:
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple"
print(fruits[0]) -- nil 😭Indexing from 1. Not from scratch. From UNIT.
Every former C programmer feels physical pain at this moment. But Lua has its reasons — the language was designed for people far from programming (engineers, designers), and for them "the first element = 1" is logical.
You get used to it in a couple of days. You suffer for the first two hours.
Useful classics: array iteration
local heroes = {"Lua", "Python", "JavaScript"}
for i, name in ipairs(heroes) do
print(i .. ". " .. name)
end
-- 1. Lua
-- 2. Python
-- 3. JavaScriptipairs — traverses by numerical indexes in order. Remember it, it will be everywhere.
Table as a dictionary: key-value on steroids
This is where the magic begins. The table can be both an array and a dictionary:
local player = {
name = "ProGamer228",
level = 42,
hp = 100,
"hidden achievement" -- это индекс [1]
}
print(player.name) -- "ProGamer228"
print(player["level"]) -- 42
print(player[1]) -- "hidden achievement"Two forms of access: via a dot (player.name) and via brackets (player["name"]). Brackets are needed when the key is a variable or contains special characters.
local key = "hp"
print(player[key]) -- 100
-- Ключом может быть вообще что угодно
local weird = {}
weird[true] = "yes"
weird[3.14] = "pi"
weird[print] = "function as a key, why not"The key can be any type except nil. This opens up crazy possibilities, but you shouldn't abuse it — code readability is also important.
Table length: it's complicated
The # operator returns the length of the "array" part of the table:
local t = {10, 20, 30, 40}
print(#t) -- 4 ✅
local mixed = {10, 20, nil, 40}
print(# mixed) -- maybe 2, maybe 4 🤡Important rule: if there is nil in the array part, the behavior of # is undefined. Lua can return any number. This is not a bug, it is by design. Just don't leave holes in the arrays.
Want a reliable length for dictionaries? Count manually:
local function tableLength(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
endTable as an object: OOP in Lua
There are no classes in Lua. But who stopped it?
local Dog = {}
Dog.__index = Dog
function Dog.new(name, breed)
local self = setmetatable({}, Dog)
self.name = name
self.breed = breed
return self
end
function Dog:bark()
print(self.name .. " says: WOOF!")
end
function Dog:info()
print(self.name .. " - breed: " .. self.breed)
end
local rex = Dog.new("Rex", "Shepherd")
rex:bark() -- Рекс говорит: ГАВ!
rex:info() -- Рекс — порода: ОвчаркаPay attention to the colon (:) instead of the dot when calling the method — this is a syntactic sugar that automatically passes the object as self. Without this, you would have to write rex.bark(rex), and it would look sad.
Inheritance? Also through tables
local Puppy = setmetatable({}, {__index = Dog})
Puppy.__index = Puppy
function Puppy.new(name, breed, toy)
local self = Dog.new(name, breed)
setmetatable(self, Puppy)
self.toy = toy
return self
end
function Puppy:play()
print(self.name .. " plays with " .. self.toy .. "!")
end
local baby = Puppy.new("Bobik", "Corgi", "ball")
baby:bark() -- Бобик говорит: ГАВ! (унаследовано от Dog)
baby:play() -- Бобик играет с мячиком!Metatables and __index are the heart of the Lua object system. When a field is not found in the current table, Lua looks in the __index metatables. The chain can be as long as you like — that's inheritance for you.

Metamethods: we overload everything
Metatables allow you to override table behavior for standard operations:
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
-- Перегружаем оператор сложения
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
-- Перегружаем вывод через tostring
function Vector.__tostring(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local v1 = Vector.new(1, 2)
local v2 = Vector.new(3, 4)
local v3 = v1 + v2
print(tostring(v3)) -- (4, 6)The main metamethods that are worth knowing:
__add,__sub,__mul,__div— arithmetic__eq,__lt,__le— comparison__tostring— string representation__index— access to a non-existent field__newindex— new field entry__call— calling a table as a function__len— overriding the operator#
With __call you can make the table callable — and this is actually used in production:
local Logger = setmetatable({}, {
__call = function(self, message)
print("[LOG] " .. os.date("%H:%M:%S") .. " — " .. message)
end
})
Logger("Server is running") -- [LOG] 14:32:01 — Сервер запущенPractical patterns: tables in real code
Config via table
local config = {
server = {
host = "localhost",
port = 8080,
},
database = {
name = "myapp",
user = "admin",
password = "hunter2", -- классика
},
debug = true,
}
print(config.server.port) -- 8080Set
local function Set(list)
local set = {}
for _, v in ipairs(list) do
set[v] = true
end
return set
end
local langs = Set{"Lua", "Python", "JavaScript", "Lua"}
if langs["Lua"] then
print("Lua in action!") -- Lua в деле!
end
if not langs["COBOL"] then
print("COBOL? No, we haven't heard of it")
endQueue
local Queue = {}
Queue.__index = Queue
function Queue.new()
return setmetatable({first = 1, last = 0}, Queue)
end
function Queue:push(value)
self.last = self.last + 1
self[self.last] = value
end
function Queue:pop()
if self.first > self.last then return nil end
local value = self[self.first]
self[self.first] = nil
self.first = self.first + 1
return value
end
local q = Queue.new()
q:push("first")
q:push("second")
print(q:pop()) -- "first"
print(q:pop()) -- "second"Top mistakes of beginners with tables
1. Forget that tables are passed by reference
local a = {1, 2, 3}
local b = a -- b указывает на ту же таблицу!
b[1] = 999
print(a[1]) -- 999 (сюрприз!)Want a copy? Do it manually:
local function shallowCopy(t)
local copy = {}
for k, v in pairs(t) do
copy[k] = v
end
return copy
end2. Confusing pairs and ipairs
ipairs— only numeric keys, in order, stops at the firstnilpairs— all keys, order is not guaranteed
3. Change the table during iteration
Don't do that. Just don't do it. Collect the changes in a separate table, and then apply them.
4. Consider # reliable for tables with holes
We've already said it, but we'll repeat it: nil in the middle of an array = unpredictable length. No options.
Learn Lua and other languages with Kodik!
If you have read up to this point, you are clearly interested not only in the theory, but also in the practice. Tables in Lua are best learned when you write code by hand, not just read articles.
Code is an application in which you can learn programming from scratch and improve your skills through practical tasks. Python, JavaScript, HTML, CSS and other technologies - all with an emphasis on the fact that you actually write code, not just look at it.
And we also have Telegram channel with a community of 2000+ developers, where useful posts, task reviews and memes about programming are regularly published (you can't do without them). A great way to repeat the material in a convenient place — in a queue, in transport, at lunch. Subscribe and level up every day.
In summary: why are tables brilliant?
Tables in Lua are like a Swiss army knife that seems strange at first, but then you don't understand how you lived without it. One data structure instead of ten, minimum syntax, maximum flexibility.
Here's what you should remember:
Tables replace arrays, dictionaries, objects, classes, and everything in general
Indexing with 1 is not a bug, but a feature (just accept it)
Metatables turn tables into a full-fledged object system
Tables are transmitted via a link — be careful with copying
#works reliably only for arrays without holes
Lua is a language that proves that less is more. And tables are the best example of this philosophy.
Now go and write something in Lua. Tables are waiting. 🚀
