OOP with Metatables

By the end of this lesson you'll build full object-oriented code in Lua without a class keyword — using a class table, __index for method lookup, a Class.new(...) constructor with setmetatable , the colon : method syntax and self , and single inheritance by chaining one class's __index to another.

Learn OOP with Metatables in our free Lua course — an interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

A class is a cookie cutter and each instance is a cookie it stamps out. The cutter (the class table) holds the shared shape — the methods every cookie should have — while each cookie carries its own dough: its own data fields. The __index link is the rule "if a cookie doesn't carry something itself, ask the cutter." Because Lua gives you tables and metatables, you don't need a special class machine: you wire up the cutter, the cookies, and that one fallback rule yourself, and full OOP falls out.

1. A Class Is Just a Table

Lua ships no class keyword, so you build one from parts you already know. Make a table for the class, set Class.__index = Class so instances can find their methods on it, and write a constructor — conventionally named new — that creates a fresh table, attaches the class as its metatable with setmetatable , fills in the instance's data, and returns it. Methods defined with a colon receive a hidden first argument, self , which is the instance. Read and run this complete little class.

2. The Colon, self, and Why They Matter

The colon is the one piece of pure syntax sugar in Lua OOP. Writing function Dog:bark() is identical to function Dog.bark(self) , and calling rex:bark() is identical to rex.bark(rex) — the colon just adds or supplies that first self argument for you. self is the receiver : the specific object the method was called on, so the same shared method can operate on many different instances. The classic mistake is calling a colon-method with a dot, which forgets self and leaves it nil .

Your turn. Finish the Circle class: wire up the metamethod, set the metatable in the constructor, and define area as a colon method. Fill in the blanks marked ___ .

3. Single Inheritance by Chaining __index

Inheritance is one more metatable. A subclass like Dog gets its own metatable whose __index points at the parent Animal , while Dog.__index = Dog keeps instance lookups hitting Dog first. The result is a lookup chain : Lua checks the instance, then the subclass, then the parent, stopping at the first match. The child constructor typically calls the parent constructor to reuse its setup, then re-points the new object's metatable to the child so the child's own methods win.

Now you try working with self . Finish the Counter so increment updates the instance's own field and value reads it back. Fill in the two blanks:

No blanks this time — just a brief and an outline. You'll combine a class table, a constructor with setmetatable , and colon methods that work on self.items . Build it, run it, and check your output against the example.

Practice quiz

Does Lua have a built-in 'class' keyword?

  • No, classes are built from tables and metatables
  • Only in Lua 5.4
  • Only with an external library
  • Yes, like Java

Answer: No, classes are built from tables and metatables. Lua has no class keyword; you assemble OOP from plain tables plus metatables.

Which metamethod makes instances find methods defined on the class?

  • __call
  • __lookup
  • __index
  • __methods

Answer: __index. When a key is missing on the instance, Lua follows __index to the class to find the method.

What is the conventional line that lets a class hold its own methods as fallbacks?

  • setmetatable(Class)
  • Class.__index = Class
  • Class.self = Class
  • Class.proto = Class

Answer: Class.__index = Class. Setting Class.__index = Class makes the class its own method table for its instances.

What does the colon in obj:method() do?

  • Looks up the metatable
  • Nothing special
  • Calls a static method
  • Passes obj as a hidden first argument named self

Answer: Passes obj as a hidden first argument named self. obj:method() is sugar for obj.method(obj); the colon supplies self automatically.

Inside a method defined with a colon, what does 'self' refer to?

  • The instance the method was called on
  • The class table
  • The global environment
  • The metatable

Answer: The instance the method was called on. self is the receiver — the specific object on which the method was invoked.

What does a constructor like Class.new(...) typically return?

  • The class table itself
  • A new table with its metatable set to the class
  • A number
  • nil

Answer: A new table with its metatable set to the class. new creates a fresh table and calls setmetatable(obj, Class) before returning it.

Which function actually links an instance to its class?

  • rawset
  • newproxy
  • getmetatable
  • setmetatable

Answer: setmetatable. setmetatable(obj, Class) attaches the class as the object's metatable.

How is single inheritance commonly implemented?

  • By copying every method into the child
  • With a special inherits keyword
  • By chaining the child class's __index to the parent
  • By calling super()

Answer: By chaining the child class's __index to the parent. You set the subclass's metatable so its __index points at the parent, chaining the lookup.

If you call a colon-defined method with a dot (obj.method()), what happens?

  • It calls the parent method
  • self is nil and indexing it errors
  • It works the same way
  • Lua passes the class instead

Answer: self is nil and indexing it errors. Without the colon, self is never passed, so it is nil and the first self.field access fails.

Why prefer Class.__index = Class over copying methods into each instance?

  • Methods are shared, saving memory and allowing later edits
  • It looks shorter only
  • It is required by Lua syntax
  • It makes instances immutable

Answer: Methods are shared, saving memory and allowing later edits. Sharing one method table means every instance reuses the same functions instead of duplicating them.