luaguides

getmetatable

getmetatable(object)

Syntax

getmetatable(object)

getmetatable returns the metatable of object, or the value of the metatable’s __metatable field if one is set. The single argument is required; calling with zero arguments raises a bad argument #1 error at the call site.

getmetatable is the read-side mirror of setmetatable. Together they form the basic stdlib interface for reflection and introspection of any Lua value, and they are the only sanctioned way to attach and read metatables from pure Lua.

Parameters

object (any, required): the value whose metatable you want to inspect. Accepts every Lua type: nil, boolean, number, string, function, userdata (full or light), thread, or table. The function never raises on type. It inspects the value’s metatable slot and applies the three-way return rule below.

Return Value

The function has three distinct outcomes, and callers have to know which one they got:

  1. nil if object has no metatable at all. This is the default for tables that have not had setmetatable called on them, and for every number, boolean, function, thread, and nil.
  2. The actual metatable table, when object is a table or full userdata whose metatable has no __metatable field. The returned table is the same reference that was set. Identity comparisons work.
  3. The value of the metatable’s __metatable field, when one is set. This is the opaque guard value chosen by the metatable’s owner, commonly a string like "locked" or a sentinel table. It is not guaranteed to be a metatable, and you cannot read metamethods through it.

The three-way contract matters: getmetatable(t) ~= nil is not the same as “I can read the metamethods” once a library has installed a __metatable guard.

Examples

Roundtrip with setmetatable

local t = {}
local mt = {
    __tostring = function(self) return "I am t" end,
}

setmetatable(t, mt)
local got = getmetatable(t)
print(got == mt)              -- true
print(getmetatable(t) == mt)  -- true (same value)
print(t)                      -- I am t  (via __tostring)

The snippet builds a fresh table, attaches a metatable that defines a __tostring metamethod, then reads the metatable back with getmetatable and confirms the identity. Both reference comparisons return true because getmetatable returns the same table object that was attached, not a copy. The third line exercises the metamethod by asking the runtime to convert t to a string. Running it prints three lines: two booleans and the string the metamethod produced. Output:

true
true
I am t

The canonical use: attach a metatable, read it back, and the reference matches. tostring here resolves through the __tostring metamethod, which is why the final print call shows a string instead of a table address.

__metatable protection

local mt = { __add = function(a, b) return "added!" end }
mt.__metatable = "locked"

local t = setmetatable({}, mt)

print(getmetatable(t))           -- locked   (not the real mt)
print(getmetatable(t).__add)     -- nil      (a string has no __add field)

local ok, err = pcall(setmetatable, t, {})
print(err)                       -- input:11: cannot change protected metatable

The example installs a metatable whose __metatable field holds the string "locked", attaches it to a table, then shows that getmetatable returns the guard value instead of the real metatable table. The pcall around setmetatable catches the error raised when the code tries to overwrite a protected metatable, which is the second half of the protection contract. Output:

locked
nil
input:11: cannot change protected metatable

The __metatable field does two things at once. It hides the real metatable from getmetatable, and it makes setmetatable raise an error. Library authors use this to keep callers from poking at internals. The contract is documented in the Lua 5.4 manual §6.1 and the metatables guide.

Strings have a built-in metatable

print(getmetatable("hello"))                       -- table: 0x...
print(getmetatable("hello").__index == string)     -- true

print(("hello"):upper())                           -- HELLO

The string library installs a shared metatable on every string value; that metatable’s __index points at the string library table. This is the whole reason ("hello"):upper() works. Method syntax on strings is a side effect of the metatable you can read with getmetatable. Trying to overwrite it with setmetatable("hi", mt) raises an error, because all strings share the same metatable.

Return is nil for un-set types

print(getmetatable({}))        -- nil
print(getmetatable(42))        -- nil
print(getmetatable(true))      -- nil
print(getmetatable(io.read))   -- nil

Tables are the only type where you commonly see a non-nil result from getmetatable in pure Lua. Numbers, booleans, functions, threads, and nil itself all have no metatable by default. The function returns nil for each.

Behavior by Type

TypeDefault metatableNotes
tablenoneSet with setmetatable; each table can have its own.
Full userdatanone (from Lua)C code attaches one. Same per-instance rules as tables.
Light userdatanoneA raw C pointer; getmetatable returns nil.
stringyes (shared)Set by the string library; __index is the string table. Read-only from Lua.
number, boolean, nil, function, threadnonegetmetatable always returns nil.

Two tables can share the same metatable object, in which case getmetatable(a) == getmetatable(b) returns true. Equality is by reference, not by structure. The weak tables guide walks through the __mode field that often rides along on shared metatables.

Common Mistakes

  • Assuming the return is always a table or nil. The __metatable branch returns whatever the owner put there: a string, a number, a sentinel table. Indexing into it without a guard check will silently return nil fields.
  • Chaining getmetatable(t).__index across a __metatable guard. If the metatable is protected, you get the guard value back, not the real __index target. Guard with pcall or check the type first.
  • Trying to setmetatable a string. Strings already have a metatable, and it is shared across every string in the program. Lua raises an error.
  • Using getmetatable as a public “is this table of class X?” check. It only works if neither side has installed a __metatable field. Many libraries do, so the check is not reliable as an API contract. Pair it with type when you need a portable test.

Probing a value’s metamethods safely

When you don’t know whether the owner has installed a __metatable guard, the naive getmetatable(t).__index lookup silently returns the wrong thing. A small helper keeps the probe honest:

local function probe_index(t)
    local mt = getmetatable(t)
    if type(mt) ~= "table" then
        return nil, "guarded"   -- __metatable set, or no metatable
    end
    local index = mt.__index
    if type(index) == "table" then
        return index, "table"
    elseif type(index) == "function" then
        return index, "function"
    end
    return mt, "raw"
end

local t = setmetatable({}, { __index = string })
print(probe_index(t))       -- table: 0x...   table

local locked = setmetatable({}, { __metatable = "no" })
print(probe_index(locked))  -- nil   guarded

The helper returns the underlying __index target when it’s reachable, plus a tag telling you how the chain is wired: a table, a function, or a raw metatable with no __index redirection. The type(mt) ~= "table" line is what makes it correct under a __metatable guard, because without it indexing into the guard value would either crash on a string or hand back the wrong object on a sentinel table.

Compatibility Notes

The three-way return contract has been stable from Lua 5.1 through 5.4. Code that handles all three branches runs on every modern interpreter. LuaJIT matches the same behaviour because it is 5.1-compatible. In Roblox’s Luau, getmetatable works identically and shows up frequently because Roblox instances are userdata with shared metatables. The Roblox scripting objects tutorial covers the patterns in practice.

If you need to look up metamethods without triggering __index chains, rawget and rawset are the raw-table access primitives that Lua itself uses internally for metatable dispatch, and grasping that distinction is what separates someone who dabbles with metatables from someone who can actually debug a third-party library whose internals refuse to leak through normal reflection.

See Also