__index and __newindex
Intercept field reads and writes with __index and __newindex metamethods.
__index and __newindex is a free Lua Academy lesson on CoddyKit. This is lesson 2 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Lua Academy learning path, and your progress syncs across the web and the CoddyKit app. The Lua Academy course includes 4 lessons in total.
__index Recap
__index is triggered when Lua tries to read a key that doesn't exist in the table. It can be a table (prototype lookup) or a function (dynamic computation). This is the most-used metamethod in Lua — it powers inheritance, default values, and lazy initialization.
local defaults = {timeout=30, retries=3}
local config = setmetatable({timeout=60}, {__index=defaults})
print(config.timeout) -- 60 (own value)
print(config.retries) -- 3 (from defaults)
print(config.missing) -- nil (not in either)__newindex Triggered
__newindex is triggered when Lua tries to write a key that does not yet exist in the table. If the key already exists, the assignment happens directly without triggering __newindex.
local proxy = setmetatable({}, {
__newindex = function(t, k, v)
print("New key:", k, "=", v)
rawset(t, k, v) -- actually store it
end
})
proxy.x = 10 -- New key: x = 10
proxy.x = 20 -- no trigger! x already exists
print(proxy.x) -- 20Read-Only Tables
Use __newindex to create read-only tables. Intercept all writes and raise an error. Combine with __index to provide values without storing them in the table itself.
local function readOnly(t)
return setmetatable({}, {
__index = t,
__newindex = function(_, k, _)
error("attempt to write to read-only table key: " .. tostring(k), 2)
end
})
end
local cfg = readOnly({host="localhost", port=8080})
print(cfg.host) -- localhost
-- cfg.host = "x" -- ERROR: attempt to write to read-only tableTracking Changes
Use __newindex to track all changes to a table — useful for debugging, change logging, or reactive data bindings. Store data in a hidden "backing" table and record changes in a log.
local log = {}
local data = {}
local tracked = setmetatable({}, {
__index = data,
__newindex = function(_, k, v)
log[#log+1] = {key=k, old=data[k], new=v}
data[k] = v
end
})
tracked.name = "Alice"
tracked.age = 30
tracked.name = "Bob"
for _, entry in ipairs(log) do
print(entry.key, entry.old, "->", entry.new)
endProxy Objects
A proxy wraps another object to intercept all reads and writes. Use an empty table as the proxy with __index and __newindex both pointing to functions. The real data lives in a separate table.
local function makeProxy(target)
return setmetatable({}, {
__index = function(_, k) return target[k] end,
__newindex = function(_, k, v)
print("Setting", k, "on target")
target[k] = v
end,
})
end
local real = {x=10}
local p = makeProxy(real)
print(p.x) -- 10 (reads from real)
p.y = 20 -- Setting y on target
print(real.y) -- 20__index Chain (Inheritance)
Chaining __index metatables creates prototype inheritance. When a key is not found in the object, Lua checks the class, then the parent class, and so on. Each level needs its own metatable with __index pointing to the next level.
local Base = {type = "base", version = 1}
local Child = setmetatable({type = "child"}, {__index = Base})
local Obj = setmetatable({}, {__index = Child})
print(Obj.type) -- base? No: child (Child has it)
print(Obj.version) -- 1 (from Base)
print(Obj.type) -- child (Child overrides Base.type)Lazy Initialization with __index
Use a function-form __index to compute and cache expensive values on first access. On the first read, compute the value and store it directly in the table (bypassing future __index invocations).
local lazy = setmetatable({}, {
__index = function(t, k)
if k == "expensiveData" then
print("Computing...")
local result = {1,2,3,4,5} -- simulate work
rawset(t, k, result)
return result
end
end
})
print(lazy.expensiveData) -- Computing... then table
print(lazy.expensiveData) -- (no "Computing" this time)__newindex for Schema Validation
Use __newindex to enforce a schema: only allow setting predefined keys or values of the correct type. This adds lightweight type safety to Lua tables.
local schema = {name="string", age="number", active="boolean"}
local function schemaTable()
local store = {}
return setmetatable({}, {
__index = store,
__newindex = function(_, k, v)
local expected = schema[k]
if not expected then error("unknown key: "..k,2) end
if type(v) ~= expected then
error(k.." must be "..expected..", got "..type(v),2)
end
store[k] = v
end
})
end
local user = schemaTable()
user.name = "Alice"
user.age = 30
-- user.age = "thirty" -- ERROR__index vs rawget
Inside a __index function, use rawget to read from the table without triggering __index again (which would cause infinite recursion). Always use rawget/rawset inside metamethods to manipulate the actual table storage.
local counter = setmetatable({count=0}, {
__index = function(t, k)
-- Use rawget to avoid recursion
local c = rawget(t, "count")
rawset(t, "count", c + 1)
return rawget(t, k)
end
})
print(counter.count) -- 0 (direct, no __index)
print(counter.missing) -- nil
print(counter.count) -- 1 (incremented)Combining __index and __newindex
The most powerful pattern: use an empty proxy table with both __index and __newindex, backed by a hidden data table. This gives full control over reads and writes, enabling features like change detection, lazy loading, and access control.
local function observable(init)
local data = init or {}
local listeners = {}
local obj = setmetatable({}, {
__index = data,
__newindex = function(_, k, v)
local old = data[k]
data[k] = v
for _, cb in ipairs(listeners) do cb(k,old,v) end
end
})
obj._onchange = function(_, cb) listeners[#listeners+1]=cb end
return obj
end
local obs = observable({score=0})
obs:_onchange(function(k,o,n) print(k,o,"->",n) end)
obs.score = 100 -- score 0 -> 100Quick Check
When is __newindex NOT triggered?
Recap: __index and __newindex
Key points:
__index: triggered on missing key read; table or function__newindex: triggered on write to new key only- Use
rawget/rawsetinside metamethods to avoid recursion - Patterns: read-only tables, change tracking, lazy init, schema validation
- Empty proxy + backing table = full read/write interception
Learn Lua with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 40
- Lessons
- 159
Frequently Asked Questions
Is the “__index and __newindex” lesson free?
Yes — any 3 lessons of the Lua Academy learning path, including “__index and __newindex”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Lua Academy course includes 4 lessons in total.
What will I learn in “__index and __newindex”?
Intercept field reads and writes with __index and __newindex metamethods. You practise Lua Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Lua Academy?
No prior experience is required. Lua Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.
How long does the “__index and __newindex” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Lua Academy lesson?
Yes. Every Lua Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Introduction to Metatables
- __index and __newindex
- Arithmetic Metamethods
- __tostring and __len