IMPORTANT: To view this page as Markdown, append `.md` to the URL (e.g. /docs/manual/basics.md). For the complete Mojo documentation index, see llms.txt.
Skip to main content
Version: 1.0.0b1
For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).

Reflection

Reflection helps you write code that inspects its own structure at compile time and reports information about types. This makes it possible to build features like structural validation, automatic comparisons, serialization, safer assertions, and richer error messages without hardcoding details for specific type implementations.

Why reflection?

Reflection is one of Mojo's powerful compile-time features. It lets you inspect types, access fields, and generate code that adapts to a struct's shape.

For example, define a struct, conform it to Equatable, and the == operator automatically works:

@fieldwise_init
struct Sensor(Equatable, Hashable, Writable):
var id: Int
var label: String
var reading: Float64

This code uses no operator overload or boilerplate.

Mojo inspects the struct at compile time, checks that each field supports equality, and generates the comparison code. That is what reflection does.

Reflection has no runtime cost. The compiler does the work up front and emits code as efficient as manually written code.

Inspect a type

Use the reflect[T]() function to inspect a type at compile time. Built into Mojo, it returns a Reflected[T] handle with methods for querying a type.

This code uses reflect[T]() to inspect a type's structure:

def show_type[T: AnyType]():
comptime r = reflect[T]()
comptime type_name = r.name()
comptime field_count = r.field_count()
comptime field_names = r.field_names()
comptime field_types = r.field_types()

print("struct", type_name)

comptime for idx in range(field_count):
comptime field_name = field_names[idx]
comptime field_type = reflect[field_types[idx]]().name()
var intro = "├──" if idx < (field_count - 1) else "└──"
print(intro, " var ", field_name, ": ", field_type, sep="")

Create some types to test this with:

@fieldwise_init
struct MyStruct:
var x: String
var y: Optional[Int]

comptime DefaultItemCount = 10

struct ParameterizedStruct[
T: Copyable, item_count: Int = DefaultItemCount
](Copyable):
var list: List[Self.T]
def __init__(out self):
self.list = List[Self.T](capacity=Self.item_count)

def main():
show_type[MyStruct](); print()
show_type[Optional[Float64]](); print()
show_type[Dict[Int, String]](); print()
show_type[ParameterizedStruct[String, item_count=5]]()

When run, this code prints each struct's name and fields with their types. The comptime for loop over fields resolves at compile time. At runtime, only the resulting print() calls execute:

struct test.MyStruct
├── var x: String
└── var y: std.collections.optional.Optional[Int]

struct std.collections.optional.Optional[SIMD[DType.float64, 1]]
└── var _value: std.utils.variant.Variant[<unprintable>, {}]

struct std.collections.dict.Dict[Int, String, std.hashlib\
._ahash.AHasher[[0, 0, 0, 0] : SIMD[DType.uint64, 4]]]
├── var _table: std.collections._swisstable\
.SwissTable[Int, String, std.hashlib._ahash\
.AHasher[[0, 0, 0, 0] : SIMD[DType.uint64, 4]]]
└── var _order: List[SIMD[DType.int32, 1]]

struct test.ParameterizedStruct[String, 5]
└── var list: List[String]

The output uses compiler-resolved names. Types appear fully qualified with parameters applied. If you only need the base type name, use base_name():

print(reflect[List[Int]]().base_name()) # List
print(reflect[Dict[String, Int]]().base_name()) # Dict

Detect field-level changes between two values

Compare two values and list which fields differ. Use this for test assertions, audit logs, change tracking, or debugging.

def diff_fields[T: AnyType](a: T, b: T) -> List[String]:
comptime r = reflect[T]()
comptime names = r.field_names()
comptime types = r.field_types()
var diffs = List[String]()

comptime for idx in range(r.field_count()):
comptime if conforms_to(types[idx], Equatable):
ref a_val = r.field_ref[idx](a)
ref b_val = r.field_ref[idx](b)
if a_val != b_val:
diffs.append(String(names[idx]))

return diffs^

For example, consider a configuration type:

@fieldwise_init
struct Config(Equatable):
var host: String
var port: Int
var verbose: Bool
var timeout: Float64

diff_fields() compares two Config values and returns the field names that differ:

def main():
var old = Config("localhost", 8080, False, 30.0)
var new = Config("localhost", 9090, True, 30.0)

var changes = diff_fields(old, new)
for name in changes:
print("changed:", name)
# changed: port
# changed: verbose

Write once, reuse everywhere with traits

Reflection is powerful when used in traits with provided methods. The method runs for any conforming struct that meets the trait constraints.

MakeCopyable duplicates every copyable field from one instance to another:

trait MakeCopyable:
def copy_to(self, mut other: Self):
comptime r = reflect[Self]()
comptime field_count = r.field_count()
comptime field_types = r.field_types()

comptime Usable = Copyable & ImplicitlyDestructible
comptime for idx in range(field_count):
comptime field_type = field_types[idx]
comptime if conforms_to(field_type, Usable):
r.field_ref[idx](other) = r.field_ref[idx](self).copy()

Conforming structs receive copy_to() without writing an implementation. As a trait method, copy_to() has direct access to Self. You don't need a type parameter.

@fieldwise_init
struct MultiType(MakeCopyable, Writable):
var w: String
var x: Int
var y: Bool
var z: Float64

def write_to[W: Writer](self, mut writer: W):
writer.write(String(t"[{self.w}, {self.x}, {self.y}, {self.z}]"))


def main():
var original = MultiType("Hello", 1, True, 2.5)
var target = MultiType("", 0, False, 0.0)

original.copy_to(target)
print(target) # [Hello, 1, True, 2.5]

You define the behavior once. Every conforming struct gets it as a provided method.

Layout, source locations, and type utilities

These tools expose lower-level details such as layout, lifetimes, and source information.

Field layout and byte offsets

When you need field layout for zero-copy serialization, C interop, or alignment, use field_offset():

struct Packet:
var flags: UInt8
var id: UInt32
var payload: UInt64

def show_layout[T: AnyType]():
comptime r = reflect[T]()
comptime names = r.field_names()
comptime for i in range(r.field_count()):
comptime off = r.field_offset[index=i]()
print(names[i], "at byte", off)

def main():
show_layout[Packet]()
# flags at byte 0
# id at byte 4 (alignment padding: 3 bytes)
# payload at byte 8

field_offset accepts name= or index= and accounts for alignment padding. The gap between flags (1 byte) and id (byte 4) shows the compiler inserting 3 bytes of padding so id aligns to a 4-byte boundary.

Types and origins

Two functions provide compile-time access to type and lifetime information from expressions:

type_of(x) returns the type of an expression for use in parameter positions:

def make_default[T: AnyType & Defaultable]() -> T:
return T()

def main():
var x = 42
var y = make_default[type_of(x)]()
print(y) # 0

origin_of(x) captures the origin (lifetime and mutability) of a reference.

In Mojo, every reference has an origin that tracks which value it borrows from and whether it can mutate that value. origin_of(x) captures this information at compile time so you can thread it through function signatures.

It appears in signatures where a returned reference must be tied to an input's lifetime:

from std.os import abort

def first_ref[
T: Copyable
](ref list: List[T]) -> ref [origin_of(list)] T:
if not list:
abort("empty list")
return list[0]

def main():
var l = [1, 2, 3]
ref x = first_ref(l)
print(x) # 1
x += 10 # modifies the original list through the reference
print(l) # [11, 2, 3]

l = []
first_ref(l) # aborts with "empty list"

The returned reference shares its origin with list, so the compiler knows it is valid as long as list is. Both are available without imports.

Source locations

call_location() returns the caller's source location, not the location of the call_location() call itself. When building assertions or validators, error messages are more useful when they point to the call site:

from std.reflection import call_location

@always_inline
def require(
cond: Bool, msg: String = "requirement failed"
) raises:
if not cond:
raise Error(call_location().prefix(msg))

def main() raises:
var x = 5
require(x > 10, "x must be > 10")
# Error: At /path/to/file.mojo:10:5: x must be > 10

The enclosing function must be @always_inline to capture the caller's location. Without this decorator, the location would point inside require(). call_location() accepts an optional inline_count parameter. The default (1) captures the immediate caller. Higher values skip additional levels of inlined calls.

source_location() returns the location where source_location() is called. This is less useful for debugging because it reports the location of the call, not the caller:

from std.reflection import source_location

def log(msg: String):
var loc = source_location()
print(
"[", loc.file_name(), ":", loc.line(), "] ",
msg, sep=""
)

def main():
log("starting up")
# [/path/to/file.mojo:4:15] starting up

Function names

Retrieve a function's source name or linker symbol at compile time. Use this for logging, tracing, or dispatch:

from std.reflection import get_function_name, get_linkage_name

def process_data():
pass

def main():
print(get_function_name[process_data]()) # process_data
print(get_linkage_name[process_data]()) # mangled symbol
  • get_function_name[func]() returns the name as written in source code.
  • get_linkage_name[func]() returns the mangled symbol name.

Both take the function as a parameter value.

Learn more