Skip to main content
    Courses/Lua/Metatables and Metamethods

    Lesson 5 • Intermediate

    Tables, Metatables & OOP

    By the end of this lesson you'll use tables as both arrays and dictionaries, customise how they behave with metatables and metamethods, and build your own classes with inheritance — the foundation of every serious Lua program.

    What You'll Learn

    • Use a table as an array and a dictionary (1-based indexing, #, insert/remove)
    • Attach a metatable with setmetatable and getmetatable
    • Provide defaults and inheritance with the __index metamethod
    • Intercept writes with __newindex
    • Overload operators with __add, __eq, and __tostring
    • Build classes with :method() colon syntax, self, and inheritance

    💡 Real-World Analogy

    Think of a table as a sheet of paper holding your data, and a metatable as a sticky note of instructions clipped to it. Normally Lua just reads the paper. But when something unusual happens — you ask for a row that isn't written down, or you try to add two sheets together — Lua reads the sticky note to find out what to do. The sticky note doesn't store data; it stores behaviour. That single idea — "look at the manual when you hit something unexpected" — is how Lua delivers default values, operator overloading, and full object-oriented programming, all from one humble data type.

    1. Tables: Lua's One Data Structure

    Lua has exactly one built-in data structure: the table. Remarkably, the same table is both an array (a numbered list) and a dictionary (named key → value pairs). The one rule that trips up newcomers: Lua arrays start at index 1, not 0. You add and remove with table.insert and table.remove, and you measure length with the # operator. Read this worked example and run it before moving on.

    Worked example: arrays and dictionaries in one type
    -- A table is Lua's ONLY data structure. It is both an
    -- array (numbered list) AND a dictionary (key -> value) at once.
    
    -- ARRAY part: a list. Lua arrays start at index 1, NOT 0.
    local fruits = { "apple", "banana", "cherry" }
    print(fruits[1])        -- prints: apple   (first item is [1])
    print(fruits[3])        -- prints: cherry
    print(#fruits)          -- prints: 3       (# is the length operator)
    
    -- table.insert adds to the end; table.remove takes from the end.
    table.insert(fruits, "date")
    print(#fruits)          -- prints: 4
    table.remove(fruits, 1) -- remove index 1 ("apple"); rest shift down
    print(fruits[1])        -- prints: banana
    
    -- DICTIONARY part: string keys instead of numbers.
    local player = { name = "Hero", level = 5, alive = true }
    print(player.name)      -- prints: Hero    (dot syntax)
    print(player["level"])  -- prints: 5       (bracket syntax — same thing)
    
    -- One table can hold BOTH parts at the same time.
    local mixed = { "first", "second", color = "red" }
    print(mixed[1])         -- prints: first   (array part)
    print(mixed.color)      -- prints: red     (hash part)
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Your turn. The snippet below is almost complete — fill in the three ___ blanks using the hints, then run it and check it against the ✅ Expected output comments.

    🎯 Your turn: index, insert, and length
    -- 🎯 YOUR TURN — replace each ___, then run this on onecompiler.com.
    
    local scores = { 10, 20, 30 }
    
    -- 1) Print the FIRST score (remember: Lua arrays start at 1)
    print(scores[___])      -- 👉 the index of the first item
    -- ✅ Expected output: 10
    
    -- 2) Add 40 to the end of the list
    table.insert(scores, ___)   -- 👉 the number to add
    
    -- 3) Print how many scores there are now
    print(___scores)        -- 👉 the length operator goes before the table
    -- ✅ Expected output: 4
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    2. Metatables & Metamethods

    A metatable is an ordinary table whose special keys — called metamethods — tell Lua what to do in situations it doesn't otherwise handle. You attach one with setmetatable(t, mt) and read it back with getmetatable(t). The most important metamethod is __index: when you read a key the table doesn't have, Lua consults __index instead of giving you nil — perfect for defaults and inheritance. Others let you intercept writes (__newindex) or redefine operators like + (__add), == (__eq), and printing (__tostring).

    Key Metamethods

    MetamethodTriggered when
    __indexYou read a missing key
    __newindexYou set a new (missing) key
    __add / __sub / __mulThe + / - / * operators
    __eq / __lt / __leThe == / < / <= operators
    __tostringtostring() or print()
    Worked example: __index, __newindex, and operator overloading
    -- A metatable is a table that customises how ANOTHER table behaves.
    -- setmetatable(t, mt) attaches the metatable mt to the table t.
    
    -- __index supplies DEFAULTS: when a key is missing on the table,
    -- Lua looks it up in __index instead of returning nil.
    local defaults = { color = "red", speed = 10, health = 100 }
    local player = setmetatable({ name = "Hero", speed = 25 }, { __index = defaults })
    print(player.name)    -- prints: Hero   (own field)
    print(player.speed)   -- prints: 25     (own field wins over the default)
    print(player.color)   -- prints: red    (missing -> falls back via __index)
    print(player.health)  -- prints: 100    (from defaults)
    
    -- __newindex intercepts SETTING a new key. Here we block writes
    -- to a "read-only" config by raising an error.
    local config = setmetatable({}, {
      __newindex = function(t, key, value)
        error("config is read-only: cannot set " .. key)
      end
    })
    -- config.volume = 11  -- would raise: config is read-only: cannot set volume
    
    -- __add overloads +, __eq overloads ==, __tostring customises printing.
    local Vector = {}
    Vector.__index = Vector
    Vector.__add = function(a, b)
      return setmetatable({ x = a.x + b.x, y = a.y + b.y }, Vector)
    end
    Vector.__eq = function(a, b)
      return a.x == b.x and a.y == b.y
    end
    Vector.__tostring = function(v)
      return "(" .. v.x .. ", " .. v.y .. ")"
    end
    
    local v1 = setmetatable({ x = 3, y = 4 }, Vector)
    local v2 = setmetatable({ x = 1, y = 2 }, Vector)
    print(tostring(v1 + v2))      -- prints: (4, 6)   (uses __add then __tostring)
    print(v1 == v1)               -- prints: true     (uses __eq)
    print(v1 == v2)               -- prints: false
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Now you try overloading operators. Finish the Money type below so that + adds two amounts and print shows a £ sign. Fill in the two blanks:

    🎯 Your turn: overload + and tostring
    -- 🎯 YOUR TURN — finish the Money type so + adds amounts.
    
    local Money = {}
    Money.__index = Money
    
    -- 1) __add should return a new Money whose amount is the sum
    Money.__add = function(a, b)
      return setmetatable({ amount = a.amount ___ b.amount }, Money)
      -- 👉 the arithmetic operator that adds two numbers
    end
    
    -- 2) __tostring should print the amount with a £ sign in front
    Money.__tostring = function(m)
      return "£" .. ___           -- 👉 the field that holds the number
    end
    
    local wallet  = setmetatable({ amount = 30 }, Money)
    local payment = setmetatable({ amount = 12 }, Money)
    print(tostring(wallet + payment))   -- ✅ Expected output: £42
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Deep Dive: __index as a table vs a function

    __index can be a table (the usual case — Lua looks the missing key up inside it) or a function that Lua calls as __index(table, key) to compute a value on demand.

    -- function form: compute or log on a missing key
    local t = setmetatable({}, {
      __index = function(tbl, key)
        return "no value for " .. key   -- instead of nil
      end
    })
    print(t.banana)   -- prints: no value for banana

    Use the table form for fixed defaults and class methods; reach for the function form when the fallback must be computed each time.

    3. Building OOP with Metatables

    Lua has no class keyword, yet it has full object-oriented programming — you assemble it from tables and metatables. The recipe: make a table for the class, set Class.__index = Class so instances inherit its methods, and write a constructor (by convention called new) that returns a table whose metatable is the class. Define methods with the colon (function Animal:speak()), which quietly adds a first parameter called self — the instance the method was called on. Inheritance is just one more metatable: point a subclass's __index at its parent.

    Worked example: a class, colon methods, self, and inheritance
    -- Lua has no 'class' keyword. You build classes from
    -- tables + metatables. Setting Class.__index = Class makes
    -- instances look up missing keys (their methods) on the class.
    
    local Animal = {}
    Animal.__index = Animal          -- methods live on Animal; instances inherit them
    
    -- A constructor is just a function that returns a table whose
    -- metatable is the class. 'new' is a convention, not a keyword.
    function Animal.new(name, sound)
      return setmetatable({ name = name, sound = sound }, Animal)
    end
    
    -- 'function Animal:speak()' is sugar for 'function Animal.speak(self)'.
    -- The colon ':' silently adds a first parameter called 'self'.
    function Animal:speak()
      return self.name .. " says " .. self.sound .. "!"
    end
    
    local cat = Animal.new("Cat", "Meow")
    print(cat:speak())     -- prints: Cat says Meow!   (cat:speak() passes cat as self)
    
    -- INHERITANCE: make Dog fall back to Animal via __index.
    local Dog = setmetatable({}, { __index = Animal })  -- Dog inherits Animal's methods
    Dog.__index = Dog                                   -- Dog instances look up on Dog
    
    function Dog.new(name)
      local obj = Animal.new(name, "Woof")  -- reuse the parent constructor
      obj.tricks = {}
      return setmetatable(obj, Dog)
    end
    
    function Dog:learnTrick(trick)
      table.insert(self.tricks, trick)      -- 'self' is the Dog instance
    end
    
    local rex = Dog.new("Rex")
    print(rex:speak())     -- prints: Rex says Woof!   (inherited from Animal)
    rex:learnTrick("sit")
    rex:learnTrick("roll")
    print(#rex.tricks)     -- prints: 2
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Common Errors (and the fix)

    • Off-by-one from 0-based habits: fruits[0] is nil in Lua — the first element is fruits[1] and the last is fruits[#fruits]. Loop with for i = 1, #t do.
    • "attempt to index a nil value (local 'self')": you called a colon-method with a dot, e.g. dog.speak(). Use dog:speak() so self is passed automatically (it equals dog.speak(dog)).
    • "attempt to call a nil value (method 'speak')": you forgot Class.__index = Class, so instances can't find their methods. Add that line right after you create the class table.
    • Mixing array and hash parts: # only counts the consecutive integer keys. In { "a", color = "red" }, #t is 1 — the color key is invisible to it. Don't rely on # for tables used as dictionaries.
    • A nil in the middle of an array: setting t[2] = nil leaves a "hole", and #t may then return 1 or 3 unpredictably. Use table.remove to delete so the rest shift down.

    📋 Quick Reference

    TaskCodeResult
    First / last itemt[1] / t[#t]1-based
    Add / removetable.insert(t, x) / table.remove(t)end of list
    Length#titem count
    Attach metatablesetmetatable(t, mt)returns t
    Defaults / inheritancemt.__index = fallbackon missing key
    Overload +mt.__add = function(a,b) enda + b
    Define methodfunction T:method() endself passed in
    Call methodobj:method()= obj.method(obj)

    Frequently Asked Questions

    Q: Why do Lua arrays start at 1 instead of 0?

    It is a deliberate design choice from Lua's authors to read more like everyday counting (the 1st item is at index 1). It catches out programmers coming from C, Java, or Python. The length operator # and table.insert/table.remove all assume 1-based indexing, so embrace it: the first element is t[1] and the last is t[#t].

    Q: What is the difference between dog.speak() and dog:speak()?

    The colon is shorthand that passes the table itself as a hidden first argument called self. dog:speak() is exactly the same as dog.speak(dog). Define methods with a colon (function Dog:speak()) and call them with a colon (dog:speak()) so self is filled in automatically. Mixing them — calling dog.speak() on a colon-defined method — leaves self as nil and crashes.

    Q: What does Class.__index = Class actually do?

    It makes the class its own fallback table. When you read a key that an instance does not have (like a method name), Lua follows __index to the class and finds the method there. Without that line, instances cannot see their own methods and you get attempt to call a nil value.

    Q: When should __index be a function instead of a table?

    Use a table for fixed defaults or class methods (the common case). Use a function — __index = function(t, key) ... end — when the fallback value should be computed on the fly, for example logging every missing-key access, or generating a value lazily the first time it is requested.

    Q: Can one table be both an array and a dictionary at the same time?

    Yes. A single table has an array part (consecutive integer keys 1, 2, 3...) and a hash part (everything else) living together. { "a", "b", color = "red" } stores "a" at [1], "b" at [2], and "red" at key "color". Just be careful: the # length operator only measures the array part and ignores string keys.

    Mini-Challenge: a Counter class

    No blanks this time — just a brief and an outline. Build the class yourself, run it, and check your output against the comment. This is the exact shape of objects you'll write in real Lua programs and games.

    🎯 Mini-Challenge: build a Counter
    -- 🎯 MINI-CHALLENGE: a Counter class
    -- 1. Make a table Counter and set Counter.__index = Counter
    -- 2. Counter.new() returns a new object with { count = 0 }
    --    whose metatable is Counter
    -- 3. Add a method Counter:increment() that does self.count = self.count + 1
    -- 4. Add a method Counter:value() that returns self.count
    --
    -- Then test it:
    --   local c = Counter.new()
    --   c:increment()
    --   c:increment()
    --   print(c:value())     -- ✅ Expected output: 2
    
    -- your code here
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    🎉 Lesson Complete!

    • ✅ A table is Lua's only data structure — array and dictionary, indexed from 1
    • table.insert, table.remove, and # manage and measure lists
    • setmetatable attaches behaviour; __index supplies defaults and inheritance
    • __newindex intercepts writes; __add/__eq/__tostring overload operators
    • ✅ Classes are tables with Class.__index = Class; :method() passes self
    • Next lesson: Lua for Game Development — put tables, metatables, and OOP to work in LÖVE2D and Roblox

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service