Mojo Struct vs Python Class: Value Semantics, Stack Allocation, and Why It Changes Everything

Mojo struct vs Python class is the first concept that breaks every Python developer’s mental model when they start writing real Mojo code. You define a struct, create an instance, pass it to a function, change a field inside that function — and nothing changes in the original. No side effect. No reference update. The struct was copied, silently, because that’s what value semantics means. In Python, that would never happen. A class instance passed to a function is a reference — change it inside, it changes everywhere. This page breaks down exactly why Mojo structs and Python classes behave differently, where each lives in memory, what the performance implications are, and the specific patterns that catch Python developers off guard every time.

Covers Mojo as of mid-2026. All examples use current struct syntax with fn and def methods.


TL;DR

  • A Mojo struct uses value semantics — assigning or passing it creates a full copy by default, unlike a Python class which passes a reference
  • Mojo structs live on the stack by default; Python class instances always live on the heap — this is the core reason structs are faster to create and destroy
  • borrowed gives you read-only access to a struct without copying; inout gives you mutable access without copying — these are the tools you reach for when you don’t want a copy
  • Mojo structs don’t support runtime inheritance — if you’re designing a type hierarchy, traits are the Mojo answer, not subclassing
  • __copyinit__ and __moveinit__ are how Mojo knows what “copy this struct” and “move this struct” actually mean — you define the behavior explicitly
  • For Python developers: think of a Mojo struct as a C struct with methods, not as a Python class with a different keyword

Mojo Struct vs Python Class: The Core Difference Is Where They Live

Python class instances always live on the heap. When you write obj = MyClass(), Python allocates memory on the heap, creates the object there, and gives you a reference to it. The variable obj is just a pointer. Pass it to a function, assign it to another variable — you’re copying the pointer, not the object. This is reference semantics, and it’s baked into every Python class unconditionally.

Mojo structs live on the stack by default. When you write var s = MyStruct(), Mojo creates the struct directly in the current stack frame. No heap allocation. No garbage collector involvement. When the function returns, the struct goes away automatically — the stack frame just shrinks. This is value semantics.

# Python — reference semantics: function modifies the original
class Point:
 def __init__(self, x, y):
 self.x = x
 self.y = y

def move(p):
 p.x += 10 # modifies the original — p is a reference

pt = Point(0, 0)
move(pt)
print(pt.x) # prints 10 — the original was changed
# Mojo — value semantics: function gets a copy, original unchanged
struct Point:
 var x: Int
 var y: Int

fn move(p: Point): # p is a copy — borrowed by default for fn
 # p.x += 10 # this would fail anyway — borrowed is read-only
 pass # even if it ran, original would be untouched

var pt = Point(0, 0)
move(pt)
# pt.x is still 0 — move() got a copy, not the original

This single difference — reference vs value, heap vs stack — is the source of almost every surprise Python developers hit in their first weeks with Mojo. It’s not a bug in your code. It’s the language working exactly as designed.

Why Does Stack Allocation Make Mojo Structs Faster?

Heap allocation requires asking the memory allocator for a block of memory, tracking that allocation, and eventually freeing it. Stack allocation requires none of this — the compiler knows exactly how much space the struct needs at compile time, reserves it in the stack frame, and reclaims it when the frame ends. No allocator call. No garbage collector tracking. For a struct created millions of times in a tight loop, this difference compounds into real, measurable performance.

Deep Dive
Mojo performance pitfalls

Debugging Mojo Performance Pitfalls That Standard Tools Won't Catch When Mojo first lands on a developer's radar, the pitch is hard to ignore: Python-like syntax, near-C performance, built-in parallelism. But once you move beyond benchmarks...

Can a Mojo Struct Live on the Heap?

Yes, when you explicitly put it there — typically by storing it inside a heap-allocated container or by using a reference type wrapper. But the default, the natural behavior, is stack allocation. You opt into heap allocation deliberately in Mojo; in Python, you never have that choice for class instances — they always go to the heap.

Mojo Struct Copy Semantics: owned, borrowed, inout Explained

This is where Python developers hit a wall, because Python has no equivalent concepts. When you pass a struct to a function in Mojo, the compiler needs to know: should it copy the struct, give read-only access, or give mutable access? You declare this explicitly, and it changes what the function can do.

borrowed is the default for fn functions. Read-only, no copy. The function sees the struct but can’t modify it. Think of it as const & from C++.

inout is mutable access without copying. Changes inside the function affect the original. This is the tool you reach for when you want the Python-style “pass and modify” behavior.

owned means the function takes ownership — the caller gives up the struct. A move happens, not a copy, when the compiler can prove the original won’t be used again.

# Mojo — borrowed vs inout: the difference in practice
struct Counter:
 var value: Int

fn read_only(c: Counter): # borrowed by default — read access, no copy
 print(c.value) # fine
 # c.value += 1 # error — borrowed is read-only

fn modify(inout c: Counter): # inout — mutable, no copy, affects original
 c.value += 1  # modifies the original Counter

var counter = Counter(0)
modify(counter)
print(counter.value) # prints 1 — inout changed the original

Without inout, a Python developer’s instinct — “pass the object, modify it inside” — doesn’t work in Mojo. The function either gets read-only access or a copy. This is a feature, not a limitation. It means every function’s relationship to its arguments is explicit and visible at the call site.

What Is the Mojo Struct Copy and Why Does __copyinit__ Exist?

When Mojo needs to copy a struct — on assignment, on passing by value — it calls __copyinit__. This is your struct’s definition of “what does it mean to copy me.” For simple structs with only value-type fields, the compiler generates this automatically. For structs that own resources — a pointer to heap memory, a file handle, a connection — you define __copyinit__ yourself to specify what a deep copy looks like.

Python never asks this question. Copying a Python object is complicated, inconsistent, and requires the copy module with copy.deepcopy() for real deep copies. Mojo makes it explicit and compiler-verified.

Mojo Struct vs Python Class Performance: Real Numbers

Creating and destroying a simple two-field Mojo struct in a tight loop — no heap allocation, stack only — is typically 5-20x faster than an equivalent Python class instance, because Python’s heap allocation, reference counting overhead, and garbage collector involvement add up on every single instance. For AI workloads that create millions of small data containers per second, this isn’t academic. It’s the difference between hitting latency targets and not.

Mojo Struct Methods vs Python Class Methods

Mojo structs can have methods — they’re not just passive data containers. But there are important differences from Python methods that trip people up.

In Python, self is always mutable — you can assign to any attribute at any time from any method. In Mojo, self in a struct method follows the same ownership rules as any other argument. By default it’s borrowed — read-only. If you want to modify self, you declare the method as mutating, which is the equivalent of marking self as inout.

# Mojo — struct methods and the mutating keyword
struct Temperature:
 var celsius: Float64

 fn get(self) -> Float64: # borrowed self — read-only, no copy
 return self.celsius

 fn set(inout self, val: Float64): # inout self — can modify self
 self.celsius = val

var temp = Temperature(20.0)
temp.set(37.0)  # inout — modifies the original
print(temp.get())  # prints 37.0

A Python developer’s first instinct is to write all methods the same way and wonder why some don’t work. The rule is simple: if the method reads data, no annotation needed. If it writes data, it needs inout self. Once that clicks, the rest of struct method design follows naturally.

Technical Reference
Mojo Traits

Mojo Traits Are Why Your AI Kernels Stop Bleeding Performance Pythons dynamic dispatch quietly eats performance in AI loops—every method call or attribute lookup adds latency, especially in heavy transformer inference. Mojo was designed to...

Does Mojo Struct Support Inheritance Like Python Class?

No — and this is deliberate. Mojo structs don’t support runtime inheritance. There’s no subclassing, no method override resolution table, no dynamic dispatch through an inheritance chain. The reason: all of that requires overhead that conflicts with Mojo’s goal of predictable, zero-cost performance.

The Mojo answer to polymorphism is traits. A struct implements a trait, and code that works on any type implementing that trait is generic over it. This is closer to Go interfaces or Rust traits than to Python class inheritance. For Python developers who use inheritance heavily, this is a genuine design shift — not just different syntax, but a different mental model.

Mojo Struct __init__ vs Python Class __init__

Both use __init__ for initialization, and the syntax looks familiar enough to be reassuring — until you notice the differences. Mojo’s __init__ must initialize every field. There’s no default-to-None behavior like Python. The compiler enforces this: if your __init__ exits without having set a field, it’s a compile error, not a runtime surprise. For Python developers used to lazy initialization and optional attributes, this feels strict at first. In practice it eliminates an entire category of “AttributeError: X has no attribute Y” bugs before the code ever runs.

Mojo Struct Value Semantics: The Patterns That Catch Python Developers

Three patterns that surprise Python developers almost every time.

Assigning a struct doesn’t share it, it copies it. var b = a creates a full copy of struct a. Modifying b after that does nothing to a. Python developers who expect b to be another name for the same object are in for a debugging mystery.

Returning a struct from a function returns a copy. Unless the compiler can optimize it away (and it often can, via move semantics), you’re returning a value, not a reference to something that lives elsewhere. The function’s local struct is gone after return — the caller gets a copy of what it was.

Putting a struct in a list or collection may copy it. Python collections hold references. Mojo collections of structs hold values. Appending a struct to a Mojo list copies the struct into the list. After that, modifying the original struct does not modify the copy in the list. This one catches people consistently.

# Mojo — value semantics in assignment: the copy surprise
struct Config:
 var timeout: Int

var original = Config(30)
var copy = original # full copy — not a reference
copy.timeout = 60 # modifies the copy only
print(original.timeout) # still 30 — original unchanged
print(copy.timeout) # 60

In Python, copy = original gives you two names pointing at the same dict or object. In Mojo, you genuinely have two separate structs in memory after that line. Neither is “the real one.” They’re independent values.

When Should You Use a Mojo Struct vs a Python-Interop Class?

Use a Mojo struct whenever you want performance, predictability, and compile-time safety — which is most of the time in Mojo code. Use Python interop (via Mojo’s Python integration) when you need to work with an existing Python library or object that expects reference semantics. Mixing both in the same file is possible but requires being explicit about which world you’re in at every boundary.

Mojo Struct Destructor: What Happens When It Goes Out of Scope

When a Mojo struct goes out of scope — function returns, block ends, variable is reassigned — the compiler calls __del__ if you’ve defined it. This is deterministic, unlike Python’s garbage collector which may collect an object at any time after its reference count drops to zero. If your struct holds a resource — a file, a network connection, allocated memory — __del__ is where you release it, and you can rely on it being called at a predictable, compiler-determined moment. No try/finally boilerplate. No context managers required.

Worth Reading
Mojo Programming Language

How the Mojo Programming Language is Redefining AI Development and Speed Python is great for prototyping. Always has been. But the moment you try to push a serious AI model into production, Python becomes the...

FAQ: Mojo Struct vs Python Class

What is the main difference between a Mojo struct and a Python class?

A Mojo struct uses value semantics — passing or assigning it creates a copy — and lives on the stack by default. A Python class instance uses reference semantics — passing it shares the same object — and always lives on the heap. This difference affects memory layout, performance characteristics, and how functions interact with the data they receive.

Why does my Mojo struct not change after I pass it to a function?

Because function arguments are borrowed by default in Mojo — read-only access with no copy allowed to modify. To modify a struct inside a function and have the changes reflected in the original, declare the argument as inout. This is explicit by design: the caller can see from the function signature whether their struct will be modified.

Does Mojo struct support inheritance?

No. Mojo structs don’t support runtime inheritance or subclassing. The Mojo mechanism for polymorphism is traits — a struct implements a trait, and generic code operates on any type that satisfies that trait. This is closer to Go interfaces or Rust traits than to Python class inheritance hierarchies.

What is borrowed in Mojo and why should Python developers care?

borrowed is Mojo’s way of passing a struct to a function for read-only access without copying it. It’s the default for fn parameters. Python developers should care because it’s the Mojo answer to “read this object without copying it” — an optimization Python never lets you make explicitly, since every class argument is already a reference share.

How do I modify a Mojo struct inside a method?

Declare the method with inout self instead of the default borrowed self. The inout keyword gives the method mutable access to the struct’s fields. Without it, any attempt to modify self inside the method is a compile error — the compiler enforces immutability of borrowed access.

Is a Mojo struct faster than a Python class?

Yes, significantly for creation-heavy workloads. Stack allocation eliminates heap allocator overhead, reference counting, and garbage collector tracking. For tight loops creating millions of small data structures — common in AI and numerical computing workloads — the difference can be 5-20x in raw allocation and deallocation speed. For I/O-bound code that creates few objects, the difference is negligible.

What is __copyinit__ in a Mojo struct?

__copyinit__ defines what happens when a struct is copied — assignment, pass by value, return by value. For structs with only simple value-type fields, the compiler generates it automatically. For structs that own heap-allocated resources, you define it to specify what a deep copy looks like. This makes copy behavior explicit and compiler-verified, unlike Python where copying objects requires the copy module with inconsistent semantics across types.

Can I use Python-style class patterns in Mojo structs?

Some patterns translate directly — __init__, methods with self, operator overloading. Others don’t: no runtime inheritance, no dynamic attribute addition, no default-None uninitialized fields, no mutable self without explicit inout. Mojo structs are not Python classes with a different keyword — they’re a fundamentally different construct that happens to share some familiar syntax.

Written by:

Source Category: Mojo Language