Skip to main content

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 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 tips and patterns to support you 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 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. The compiler uses them to generate fast, specialized machine code.
  • 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, with no interpreter overhead.
  • Mojo prefers value semantics and explicit mutability. In Python, most objects are mutable references, so assigning a list doesn't copy it. In Mojo, assigning or passing a value typically creates an independent copy. Changes to one value don't affect others unless you make sharing explicit. This keeps data flow clear and enables safe parallelism.
  • Mojo supports modern ownership, but it doesn't trap you in "safe-only" abstractions. With ownership, the compiler tracks which variables and fields control a value's lifetime. That lets Mojo manage memory effectively, without a garbage collector or reference counting. When you need low-level control, such as interfacing with C-language libraries, you can manage memory explicitly using UnsafePointer.
  • Mojo brings together ideas from Rust, C++, and Python. It combines Python's readability with a performance model inspired by systems languages like Rust and C++.

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 explore 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 the same 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 list

In Mojo, assignment gives you a copy. If you're not working with trivial types (like Int or Bool) or types with built-in copy semantics (like String), you may need to use an explicit copy call. You can also use and create types that are implicitly copyable.

var a = "hello"  # hello
var b = a        # hello, implicit copy
b = b + " world" # hello world
print(a)         # hello
print(b)         # hello world

var c: List[Int] = [1, 2, 3]
var d = c.copy() # d is an independent copy
d.append(4)      # [1, 2, 3, 4]
print(c)         # [1, 2, 3]  c is unchanged

Mojo uses var to declare variables. A var binding owns its value. Using var consistently makes your code easier to read. It's clear when you introduce a new binding and when you reassign an existing one.

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 20

Mojo

All variables are mutable by default. Function arguments aren't mutable by default.

var x = 10 # 10
x = 20     # 20, with a warning that x's previous assignment
            # to 10 was never used

def 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. Using the mut keyword in the argument declaration makes it mutable.

def foo(mut value: Int):
    value += 1 # This works

Explicit mutability makes code easier to reason about because mutability is visible. You can look at a function signature and immediately see which values can change and which can't.

Numbers

Python gives you int and float with arbitrary precision. Mojo takes a different approach: it uses explicit, fixed-width numeric types so the compiler can optimize aggressively and scale across parallel execution.

Mojo provides concrete numeric types like Int8, Int16, Int32, Int64, Float16, Float32, and Float64. Fixed width isn't a limitation in Mojo, it's a feature that lets the compiler pack numbers with known sizes into memory.

SIMD types

In many languages, SIMD shows up later as a specialized tool for advanced users. In Mojo, SIMD is part of the core compute model. Mojo implements its primitive numeric types as SIMD values under the hood.

That lets the compiler operate on multiple values with a single hardware instruction. When you choose the right numeric type, you give the compiler more room to generate faster code.

Int types

Python

Python's int uses arbitrary precision. It can hold integers as large as memory allows.

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 std.sys import size_of

def main():
    var a: Int = 5
    var bytes = size_of[Int]()
    print(bytes)  # 8 on a 64-bit system

Floating point types

Python

In Python, float is always 64-bit.

Mojo

Mojo floating point types aren't arbitrary precision. Mojo doesn't provide a default floating point type, the way it does with integers. That means there's no built-in Float type.

When you're just starting with Mojo, stick to Float32 or Float64 floating point.

Division and types

Mojo division behaves differently than in Python. In Python, dividing two integers always produces a float:

a = 7
b = 2

print(7 / 2)   # 3.5 — always float

In Mojo, if you want integer division to return a floating-point result, you must use explicit casting:

var a: Int = 7
var b: Int = 2

print(Float64(a) / Float64(b))     # 3.5 — explicit float division

Mojo division operators

In Mojo, / returns a value that always matches the type of the operands. A floating point number divided by a floating point number returns a floating point number, and an integer divided by an integer returns an integer:

var a: Int = 7
var b: Int = 2

print(a / b)   # 3, result type matches operand type

Python programmers may be a bit surprised that / isn't "true division." It returns a truncated result, but the result is biased towards zero:

var c = -7
var d = 2
print(c / d)  # -3, not -4, truncates towards zero

// performs floored division, in the direction of negative infinity:

print(c // d) # -4, not -3, truncates towards negative infinity

Like /, the type returned by // is preserved from the operands:

var e: Float64 = 7.0
var f: Float64 = 2.0
print(e // f) # 3.0, floors toward negative infinity
print(e / f)  # 3.5

var g: Float64 = -7.0
print(g // f) # -4.0, floors toward negative infinity
print(g / f)  # -3.5

Data 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 element type. In this example, that type is Int. This allows the list implementation to pack data efficiently, using less space and improving performance. Mojo uses packed data rather than indirect references:

var nums: List[Int] = [1, 2, 3]
nums.append(4) # [1, 2, 3, 4]

To store different kinds of values, define the element type as a Variant that enumerates permitted types. Variant lets a single element type represent multiple concrete value types:

from std.utils import Variant

comptime MixedType = Variant[Int, Float64, String, Bool]

var mixed_list = List[MixedType]()
mixed_list.append(MixedType(42))
mixed_list.append(MixedType(3.14))
mixed_list.append(MixedType("hello"))
mixed_list.append(MixedType(True))

for item in mixed_list:
    print(item) # Output lines: 42, 3.14, hello, and True

Variant tells the Mojo compiler which types are used, so it can allocate and manage memory correctly.

Data structures: dictionaries

Python dicts are dynamic. Keys and values can be anything. Mojo uses efficient dictionary implementations (Swiss tables) for fast data storage and retrieval. Typed dictionaries support efficient packing and data access.

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 element types to expect for keys and values. This enables tighter, faster code. As with other Mojo collections, you can use Variant to broaden the range of permitted element types:

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.

Mojo

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}, set

Iteration

In terms of syntax, Mojo's for and while loops align with Python. Use break and continue for control flow.

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]

Function definitions

In Python, you can write a function without specifying the types of its arguments or return value. In Mojo, you must declare types explicitly.

Python

def add(a, b):
    return a + b

Mojo

def add(a: Int, b: Int) -> Int:
    return a + b

The optional -> syntax declares the return type.

Error handling

Error handling in Mojo looks very similar to Python. You raise and catch exceptions.

Python

try:
    raise ValueError("bad input")
except ValueError as e:
    print(e)  # bad input

The raises keyword in function and method declarations indicates that a function may generate or propagate errors.

Mojo

try:
    raise Error("bad input")
except e:
    print(e)  # bad input

You can specify error types by adding a type name after the raises keyword. This lets you catch the error and use the type instance directly in your except clause:

@fieldwise_init
struct MyCustomError(Writable):
    var message: String

def test_typed_error() raises MyCustomError: # Typed error
    raise MyCustomError("custom error occurred")

try:
    test_typed_error()
except e:
    print(e.message) # custom error occurred

Functions that don't handle the errors they raise automatically delegate error handling to their caller. You must declare these functions with the raises keyword:

def another_raising_function() raises:
    raise Error("Message")     # Error raised here

def raising_function() raises:
    another_raising_function() # Error continues to pass

def handles_errors():
    try:
        raising_function()     # Error handled in this non-raising function
    except e:
        # handle error here

Note that 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 = y

Mojo

Mojo initializers require out self. The out keyword indicates that the method returns a value through an argument. In initializers, that argument is self, and the instance's fields are guaranteed to be fully initialized:

struct Point:
    var x: Int
    var y: Int

    def __init__(out self, x: Int, y: Int):
        self.x = x
        self.y = y

...

def main():
    var point = Point(5, 3)
    print(point.x, point.y) # 5, 3

Structs 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 error

Mojo

In Mojo, once a name is bound in a scope, its type is fixed and can't be rebound to a different type:

var a = 1    # Int
a = "string" # Error: can't implicitly convert String to Int

When you use Variant, you can switch between the types it enumerates. The variable itself remains statically typed as a Variant, even though the concrete value it holds may change:

from std.utils import Variant
from std.testing import *

comptime StringOrInt = Variant[String, Int]

var a: StringOrInt = 1 # Initial value, 1
assert_true(a.unsafe_get[Int]() == 1)

a = "string" # Not an error, "string"
assert_true(a.unsafe_get[String]() == "string")

Types 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.

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 sketch(shape):
    shape.draw()  # Works if shape has draw(), fails at runtime if not

Mojo traits

trait Drawable:
    def draw(self):
        ... # required method

def sketch[T: Drawable](shape: T):
    shape.draw()  # Compiler guarantees shape has `draw()`

Memory management

In Python, memory access is indirect, which adds overhead. Mojo uses direct memory access, speeding up execution.

Python manages memory automatically using reference counting and a garbage collector. Reference counting deallocates objects when their count reaches zero. The garbage collector runs in the background to clean up objects that are no longer reachable. This removes the need to manage memory manually, but it also means you have no control over when collection happens.

Mojo uses ownership semantics with ASAP ("as soon as possible") destruction. The compiler knows exactly when a value is used for the last time and its lifetime ends. Memory is freed at that point, without waiting for a garbage collector to run.

If you need manual memory control, Mojo offers a suite of pointer and allocation options. You get convenience by default, and precise alloc() and free() 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 can leave performance, correctness, readability, and maintainability 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 supports parallelism at multiple levels, from data-parallel SIMD operations to multi-threaded GPU execution. This isn't limited to threads. Mojo enables low-level SIMD parallelism and higher-level parallelism across GPUs.

"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, ownership, and value semantics. You don't have to use all of it at once, but it's there for when you need it.

Was this page helpful?