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

Lua tables: the most powerful structure that beginners underestimate

We understand why tables are the only data structure in Lua — this is not a limitation, but a superpower. Arrays, dictionaries, objects, classes, and even queues — all of these are tables. With code examples, metatables, typical beginner mistakes, and memes.

К

Kodik

Author

8 min read

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.

🔥 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

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. JavaScript

ipairs — 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
end

Table 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)  -- 8080

Set

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")
end

Queue

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
end

2. Confusing pairs and ipairs

  • ipairs — only numeric keys, in order, stops at the first nil

  • pairs — 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. 🚀

🎯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