Mojo tips for Python Devs
Mojo is designed with Python programmers in mind, but it isn't "just Python, only faster." Mojo introduces a type system, ownership-aware semantics, and low-level control that, as a Python developer, you may not have had to reason about to make your code work.
This guide offers a practical resource for experienced Python developers. It shows how familiar Python patterns translate into Mojo, where your instincts still apply and where Mojo asks you to build a new mental model.
This isn't a tutorial. It's a core set of language migration signposts as you migrate to Mojo.
Mojo's core model for Python developers
Mojo looks like Python but its execution model is closer to Rust, Swift, C++, and other MLIR-optimized systems languages. Key differences from Python include:
- Mojo is statically typed. In Python, types are optional hints that the interpreter mostly ignores at runtime. In Mojo, types are first-class citizens. The compiler uses them to generate fast, specialized machine code.
- Mojo uses value semantics and explicit mutability. In Python, almost everything is a mutable reference, so assigning a list doesn't copy it. Mojo uses value semantics. In Mojo, assigning or passing a value establishes an independent copy. Changes to one value don't affect others unless sharing is made explicit. This makes data flow clear and enables safe parallelism.
- Mojo supports ownership and C-like manual management. With
ownership, the compiler tracks which variables and fields are
responsible for a value's lifetime. This lets Mojo manage memory
without a garbage collector. For low-level work, such as interfacing
with C libraries, Mojo also supports explicit memory management through
its
UnsafePointerAPI. - Mojo compiles to machine code. Python runs through an interpreter that translates your code at runtime, adding overhead to every operation. Mojo compiles directly to native machine code. This gives you fast and predictable performance, no interpreter overhead, and no GIL, the Global Interpreter Lock that forces Python to run only one thread at a time even on multi-core hardware.
- Mojo is multi-paradigm. Mojo borrows Python's syntax, not its philosophy. There are no dynamic classes and no garbage collection, just structs, ownership, and compile-time guarantees. Think Python's readability, Rust's performance model.
Understanding these differences is essential for writing correct, fast Mojo.
Moving from Python to Mojo
This section introduces a curated set of migration topics that introduce common Mojo patterns.
Value semantics
In Mojo, when you assign a value to a new variable, it's given a unique owned value, not a second reference pointing to the same data. This can catch new adopters off guard.
In Python, both a and b refer to one list:
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]
# a changed because b and a both point to the same listIn Mojo, assignment gives you a copy. If you're not working with trivial types (like Int or Bool), you may need to use a copy call. You can also use and create types that are implicitly copyable.
var a: List[Int] = [1, 2, 3]
var b = List[Int](copy=a) # b is an independent copy
b.append(4)
print(a) # [1, 2, 3] a is unchanged
var c = "hello" # hello
var d = c # hello, implicit copy
d = d + " world" # hello world
print(c) # hello
print(d) # hello worldThis is value semantics. Your data doesn't change just because
another variable was assigned. To use Python-like reference behavior,
declare b with ref instead of var :
var a: List[Int] = [1, 2, 3]
ref b = a # b is a reference to the same value
b.append(4) # The list updates. a still owns the list.
print(a) # [1, 2, 3, 4]Mutability
In both Python and Mojo, almost everything you create can be changed after the fact. In Mojo, there are some special rules.
Python
Nearly everything is mutable.
a = 10 # 10
b = a # 10
b = b + 10 # 20
print(a, b) # 10 20Mojo
All variables are mutable by default. Function arguments aren't mutable by default.
var x = 10 # 10
x = 20 # 20, with a warning that assignment to 10 was never used
fn foo(value: Int):
value += 1 # Error, expression must be mutable
var y = 20
foo(y)Default immutability in the function gives the compiler more room to
optimize and makes concurrent code safer, since immutable values can't
be accidentally changed by multiple threads. Using the mut keyword in
the argument declaration makes it mutable.
fn foo(mut value: Int):
value += 1 # This worksNumbers
Python has int and float of arbitrary precision. Mojo uses types
that are specific, and integrate into parallel execution
because performance and optimization are important language features.
Mojo number types
Mojo uses explicit, fixed-width numeric types, such
as: Int8, Int16, Int32, Int64, Float16, Float32, Float64.
These fixed-width types aren't just a constraint, they're a feature.
Mojo's numeric types are built on SIMD, which means the compiler can
operate on multiple values in a single hardware instruction. Choosing
the right type gives the compiler room to generate faster code.
Mojo's Int type
The general Int type maps to your machine's native word size. This is
typically 64 bits on a 64-bit system. You can always check:
from sys import size_of
fn main():
var a: Int = 5
var bytes = size_of[Int]()
print(bytes) # 8 on a 64-bit systemMojo doesn't provide a floating point analog (that is, it fits to the
machine) to its general integer type. That means there's no Float
type.
Python
Python's int is arbitrary precision. It can hold any integer, as
large as memory allows. float is always 64-bit. Division between two
integers always returns a float:
print(7 / 2) # 2.5 — always float
print(7 // 2) # 3 — floor division, returns intMojo
Division behaves differently than Python. Dividing two integers
returns a truncated integer. Use / for integer division, and
cast explicitly when you need a float result:
var a: Int = 7
var b: Int = 2
print(a / b) # 3 - integer division, truncates
print(Float64(a) / Float64(b)) # 3.5 — explicit float divisionData structures: lists
Python
In Python, lists are dynamic. They can hold mixed types and grow freely.
nums = ["one", 2.0, 3]
nums.append(4) # ['one', 2.0, 3, 4]Mojo
A typed list holds only one kind of value. In this example, it's Int.
The compiler uses this constraint to generate faster code.
var nums: List[Int] = [1, 2, 3]
nums.append(4) # [1, 2, 3, 4]Data structures: dictionaries
Python dicts are dynamic. Keys and values can be anything. Mojo supports Python-style dicts for compatibility and typed dicts for performance. Typed dicts use Swiss tables to improve the efficiency of data storage and retrieval.
Python
counts = {"a": 1, "b": "two"}
counts["c"] = 3.0 # {'a': 1, 'b': 'two', 'c': 3.0}Mojo
A typed declaration like Dict[String, Int] tells the compiler exactly
what kinds of keys and values to expect. This supports tighter, faster
code.
var counts: Dict[String, Int] = {"a": 1, "b": 2}
counts["c"] = 3 # {a: 1, b: 2, c: 3}Comprehensions
Python's comprehensions have direct Mojo analogs. The syntax is essentially identical.
Python
squares = [x*x for x in [0, 1, 2, 3, 4] if x % 2 == 0]
# [0, 4, 16], list
positive_numbers = [x for x in range(-3, 3) if x > 0]
# [1, 2], list
{x: x * x for x in range(3)}
# {0: 0, 1: 1, 2: 4}, dict
{k: v.upper() for k, v in [(1, "one"), (2, "two")]}
# {1: 'ONE', 2: 'TWO'}, dict
number_set = {x for x in range(5)}
# {0, 1, 2, 3, 4}, setMojo
var list_squares = [x * x for x in [0, 1, 2, 3, 4] if x % 2 == 0]
# [0, 4, 16], list
var positive_numbers = [x for x in range(-3, 3) if x > 0]
# [1, 2], list
var dict_squares = {x: x * x for x in range(3)}
# {0: 0, 1: 1, 2: 4}, dict
var upper_case = {k: v.upper() for k, v in [(1, "one"), (2, "two")]}
# {1: ONE, 2: TWO}, dict
var number_set = {x for x in range(5)}
# {0, 1, 2, 3, 4}, setIterations
In terms of syntax, Mojo's for and while loops align with
Python.
Mojo using a typed for-loop
var nums = [0, 1, 2, 3, 4]
var squares2: List[Int] = []
for x in nums:
if x % 2 == 0:
squares2.append(x * x)
print(squares2) # [0, 4, 16]Mojo using a while loop
var squares3: List[Int] = []
var idx = 0
while idx < 3:
squares3.append(idx * idx)
idx += 1
print(squares3) # [0, 1, 4]Functions definitions
In Python, you can write a function without specifying the types of its arguments or return value. In Mojo, you declare types explicitly. That's how Mojo knows how to compile the function efficiently.
Python
def add(a, b):
return a + bMojo
fn add(a: Int, b: Int) -> Int:
return a + bThe -> syntax declares the return type. When omitted, it returns
None, a special Mojo term that represents the absence of a value.
Error handling
Mojo handles errors the same way Python does. It raises and catches exceptions.
Python
try:
raise ValueError("bad input")
except ValueError as e:
print(e) # bad inputMojo
try:
raise Error("bad input")
except e:
print(e) # bad inputRaising typed errors in functions and methods
Mojo also supports typed errors by declaring a type after the raises
keyword. The raises keyword in function and method declarations
indicates that they may generate or propagate errors.
@fieldwise_init
struct MyCustomError(Writable):
var message: String
fn test_typed_error() raises MyCustomError: # Typed error
raise MyCustomError("custom error occurred")
try:
test_typed_error()
except e:
print(e) # MyCustomError(message=custom error occurred)Functions that don't handle the errors they raise automatically delegate error handling back to their caller.
In Mojo, each try/except statement can handle a single error type.
Types: classes vs structs
Python classes are flexible and dynamic. You can add attributes at
runtime, mix types, and override behavior freely. Mojo uses struct, a
statically typed alternative that the compiler can optimize
aggressively.
Mojo structs are stack-allocated. The value lives in a fast, fixed-size region of memory rather than on the heap, where a garbage collector must track and clean it up.
Python
class Point:
def __init__(self, x, y):
self.x = x
self.y = yMojo
struct Point:
var x: Int
var y: Int
fn __init__(out self, x: Int, y: Int):
self.x = x
self.y = y
...
fn main():
var point = Point(5, 3)
print(point.x, point.y) # 5, 3Structs deliver performance and predictability.
Types: variable static typing
Python
In Python, you may assign different types to the same variable.
a = "x" # String
a = 10 # Not an errorMojo
In Mojo, once you use a name in a scope, it's statically typed and can't be re-bound to a different type:
var a = 1 # Int
a = "string" # Error: can't implicitly convert String to IntCreating a subordinate scope allows you to re-use the symbol with a different type.
var a = 1
fn foo() raises:
var a = "hello"
print(a)
foo() # hello
print(a) # 1Types and polymorphism: duck typing vs. traits
In Python, duck typing means you don't declare what interface an object must have. If it has the method you call, it works at runtime. This is flexible, but it gives you no safety net at build time.
Mojo uses traits to solve the same problem explicitly. A trait defines the methods a type must implement or provides a default implementation. The compiler verifies that any type used in that role actually provides those methods, so mistakes surface before your code runs.
Python duck typing
def draw(shape):
shape.draw() # Works if shape has draw(), fails at runtime if notMojo traits
trait Drawable:
fn draw(self):
... # required method
fn test_draw[T: Drawable](shape: T):
shape.draw() # Compiler guarantees shape has `draw()`Memory model
Python manages memory automatically through a garbage collector, a background process that tracks which objects are no longer in use and frees their memory. You never have to think about it. The tradeoff is that you have no control over when collection happens or how memory is laid out.
Mojo uses ASAP ("as soon as possible") memory management. The compiler knows exactly when a value is used for the final time and its lifetime ends. This frees memory at the right moment without waiting for a collector to run.
Python
- The garbage collector handles memory automatically.
- Everything is a heap-allocated reference.
Mojo
- ASAP memory management doesn't wait to free memory that's no longer needed. It frees memory as soon the compiler knows a value won't reappear.
- If you need manual control, Mojo offers a suite of pointer and
allocation options. You get convenience by default, and precise
alloc()andfree()control when you reach for it.
Python instincts that may surprise you in Mojo
"I don't need types."
In Python, that's often fine. In Mojo, types are how the compiler generates fast, optimized code. Untyped code works in dynamic languages like Python, but it leaves performance on the table.
"Everything is mutable."
In Mojo, variables are mutable by default, but function and method arguments may not be.
"I can mix types in a list."
Mojo collections use static types and benefit from the performance that brings.
"Threads are how I parallelize."
Python threads are limited by the GIL. Mojo focuses on high-performance GPU code, which is required for machine-learning and other high-performance compute workloads.
"Classes are the natural way to structure things."
Mojo's struct value type and its traits offer a better fit for
performance-sensitive code.
The Mojo mindset
Mojo gives you Pythonic ergonomics with systems-level control. That control comes when you embrace types, memory lifetimes, and value semantics. You don't have to use all of it at once, it's there for when you need it.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!