PONY λ M2 Modula-2

Python.CodeCompared.To/Mojo

An interactive executable cheatsheet comparing Python and Mojo

Python 3.13 Mojo 0.26.1
Output & Running
Hello, World
print("Hello, World!")
fn main() raises: print("Hello, World!")

Mojo's print() works exactly like Python's: it accepts any number of values, joins them with spaces, and adds a newline. The entry point is fn main() raises: rather than the implicit top-level execution Python uses. The raises qualifier allows the function to propagate errors freely — a useful habit for main() so any called function can use raise without an additional declaration.

Printing multiple values
name = "Alice" age = 30 print(name, age) print(name, "is", age, "years old")
fn main() raises: var name = "Alice" var age = 30 print(name, age) print(name, "is", age, "years old")

Mojo's print() accepts multiple comma-separated arguments and joins them with spaces — identical to Python's print(). The only visible difference in this example is the var keyword on the first two lines. Mojo nightly has added f-string support, but the multi-argument form is used here because it works across all Mojo versions and makes it clear that no implicit type coercion is occurring.

Comments
# Single-line comment print("Hello") # inline comment """ Multi-line string often used as a block comment """
fn main() raises: # Single-line comment print("Hello") # inline comment # Mojo has no block comment syntax — # use consecutive line comments instead.

Mojo uses # for line comments, identical to Python. Unlike Python, Mojo has no triple-quoted string literal that can double as a block comment — """...""" is not valid in fn function bodies. Use multiple consecutive line comments for longer explanations.

String conversion for output
value = 42 pi = 3.14159 print("Value: " + str(value)) print("Pi: " + str(pi))
fn main() raises: var value: Int = 42 var pi: Float64 = 3.14159 print("Value: " + String(value)) print("Pi: " + String(pi))

Python's built-in str() converts any value to a string for concatenation. Mojo uses an explicit String(value) constructor call that works the same way. Both produce a human-readable string representation. Alternatively, passing values as separate print() arguments avoids the conversion step entirely and is the more common Mojo idiom.

Variables & Types
Variable declaration
name = "Alice" count = 42 active = True print(name, count, active)
fn main() raises: var name = "Alice" var count = 42 var active = True print(name, count, active)

Mojo requires the var keyword to introduce a new variable inside an fn function body. Bare assignment to an undeclared name is a compile-time error — unlike Python, where it silently creates a new variable. Types are inferred from the initial value exactly as in Python, so no annotation is needed when the type is obvious from context.

Type annotations
name: str = "Alice" age: int = 30 score: float = 9.5 print(name, age, score)
fn main() raises: var name: String = "Alice" var age: Int = 30 var score: Float64 = 9.5 print(name, age, score)

Python's type hints (name: str = "Alice") are advisory — the interpreter ignores them at runtime. Mojo's type annotations in fn functions are enforced at compile time: assigning the wrong type is a compile error. Mojo uses String (capital S) rather than Python's str, and Float64 rather than float. Plain Int is machine-word-sized (64-bit on modern hardware).

Reassignment
counter = 0 counter = 10 counter += 5 print(counter) # 15
fn main() raises: var counter = 0 counter = 10 counter += 5 print(counter) # 15

Once a var variable is declared, reassignment uses plain = without the var keyword — the same as Python. Compound assignment operators (+=, -=, *=, /=) all work as expected. The difference from Python is only at the point of initial declaration, where var is required.

Constants
# Python has no built-in constant mechanism; # convention is to use ALL_CAPS names (but nothing enforces it) MAX_CONNECTIONS = 100 APP_NAME = "MyApp" print(MAX_CONNECTIONS, APP_NAME)
alias MAX_CONNECTIONS: Int = 100 alias APP_NAME: String = "MyApp" fn main() raises: print(MAX_CONNECTIONS, APP_NAME)

Mojo's alias keyword creates a compile-time constant — the value is substituted at compile time and cannot be changed at runtime. Python only has a naming convention (ALL_CAPS) that other programmers are supposed to respect; nothing prevents reassignment. Mojo alias values can appear in type parameter positions where only compile-time-known values are allowed, such as SIMD vector widths.

Type conversion
whole = 10 decimal = float(whole) / 3.0 text = str(whole) print(decimal) print(text + " items")
fn main() raises: var whole: Int = 10 var decimal: Float64 = Float64(whole) / 3.0 var text: String = String(whole) print(decimal) print(text + " items")

Python's built-in conversion functions (int(), float(), str()) accept almost any value and handle conversion automatically. Mojo uses explicit constructor-style calls: Float64(whole), String(whole). Mojo never performs implicit numeric coercion — mixing Int and Float64 in an expression without an explicit conversion is a compile error.

Strings
String basics
greeting = "Hello" name = "World" combined = greeting + ", " + name + "!" print(combined) print(len(combined))
fn main() raises: var greeting = "Hello" var name = "World" var combined = greeting + ", " + name + "!" print(combined) print(len(combined))

Mojo's String type behaves much like Python's: + concatenates strings, and len() returns the byte length. All Mojo strings are UTF-8 encoded and immutable by default, matching Python's string immutability. One key difference: Mojo's len() counts bytes, not Unicode code points, so multi-byte characters produce a larger count than Python's character-counting len().

Case conversion
message = "Hello, World!" print(message.upper()) print(message.lower())
fn main() raises: var message = "Hello, World!" print(message.upper()) print(message.lower())

Mojo's String type provides the same .upper() and .lower() methods as Python. Both return a new string without modifying the original — string immutability is the same in both languages. These method names were intentionally chosen to match Python's, easing the transition.

Find and replace
sentence = "The quick brown fox" print("quick" in sentence) print(sentence.find("fox")) print(sentence.replace("fox", "cat"))
fn main() raises: var sentence = "The quick brown fox" print(sentence.find("quick") != -1) print(sentence.find("fox")) print(sentence.replace("fox", "cat"))

Mojo's .find() returns the byte index of the first occurrence, or -1 if not found — the same semantics as Python's str.find(). Python's in operator for containment checking has no direct single-method equivalent in Mojo; the idiom is .find() != -1. The .replace() method replaces all occurrences, matching Python's behaviour.

Strip and split
padded = " hello " cleaned = padded.strip() print(cleaned) words = "one two three".split() print(len(words))
fn main() raises: var padded = " hello " var cleaned = padded.strip() print(cleaned) var words = "one two three".split() print(len(words))

.strip() removes leading and trailing whitespace and .split() without arguments splits on whitespace runs — both behave identically to Python. Mojo's .split() returns a List[String] rather than Python's list, but the usage is the same: index it with [i] or iterate with a for loop.

Prefix and suffix checks
filename = "report.csv" print(filename.startswith("report")) print(filename.endswith(".csv")) print(filename.endswith(".txt"))
fn main() raises: var filename = "report.csv" print(filename.startswith("report")) print(filename.endswith(".csv")) print(filename.endswith(".txt"))

.startswith() and .endswith() have identical names and semantics in Mojo and Python. Both return a Bool value. This is one of the areas where Mojo's Python-superset design makes the transition completely seamless — the code is character-for-character identical.

Numbers
Integer types
# Python has one integer type: arbitrary precision small = 42 large = 10 ** 100 # googol — no overflow in Python print(type(small).__name__) print(small)
fn main() raises: var machine_word: Int = 42 var signed_64: Int64 = 1_000_000_000 var signed_32: Int32 = 32_767 var signed_8: Int8 = 127 print(machine_word) print(signed_64)

Python's int has arbitrary precision — it grows as large as memory allows. Mojo's integer types have fixed bit widths: Int8, Int16, Int32, Int64, and their unsigned counterparts (UInt8, etc.). Plain Int is machine-word-sized (64-bit on a 64-bit system). Unlike Python, Mojo integers can overflow — an Int8 holding 128 wraps in release builds and panics in debug builds.

Float types
# Python has one float type: IEEE 754 double-precision (64-bit) pi = 3.14159265358979 tau = 6.28318530717958 print(type(pi).__name__) print(pi + tau)
fn main() raises: var pi: Float64 = 3.14159265358979 var approx: Float32 = 3.14 var half_prec: Float16 = 3.14 print(pi) print(approx)

Python's float is always IEEE 754 double-precision (64-bit). Mojo provides three floating-point types: Float16, Float32, and Float64. Floating-point literals default to Float64, matching Python's behaviour. Use Float32 or Float16 to match the precision expected by GPU and SIMD hardware in machine-learning workloads — the primary domain Mojo targets.

Arithmetic operators
a = 17 b = 5 print(a + b) # 22 print(a - b) # 12 print(a * b) # 85 print(a // b) # 3 (floor division) print(a % b) # 2 (modulo) print(a ** 2) # 289 (exponentiation)
fn main() raises: var a = 17 var b = 5 print(a + b) # 22 print(a - b) # 12 print(a * b) # 85 print(a // b) # 3 (floor division) print(a % b) # 2 (modulo) print(a ** 2) # 289 (exponentiation)

Mojo's arithmetic operators are identical to Python's: +, -, *, // (floor division), % (modulo), and ** (exponentiation). This is intentional — Mojo is a Python superset. The difference is that Mojo resolves these at compile time when types are known, potentially emitting a single CPU instruction instead of a Python bytecode dispatch chain.

Bool type
is_active = True is_done = False print(is_active) print(not is_active) print(is_active and not is_done)
fn main() raises: var is_active: Bool = True var is_done: Bool = False print(is_active) print(not is_active) print(is_active and not is_done)

Mojo's Bool type uses the same capitalized literals (True, False) and the same English-word operators (and, or, not) as Python. This is one of the areas where the transition from Python to Mojo is effortless — the code looks identical. The difference is that Bool in Mojo is a strictly typed value, not a subclass of int as it is in Python.

Collections
List basics
numbers = [10, 20, 30, 40] print(numbers[0]) # 10 print(numbers[-1]) # 40 print(len(numbers)) # 4
fn main() raises: var numbers = List[Int]() numbers.append(10) numbers.append(20) numbers.append(30) numbers.append(40) print(numbers[0]) # 10 print(numbers[len(numbers) - 1]) # 40 print(len(numbers)) # 4

Mojo's List[T] is a typed, resizable array. Unlike Python's heterogeneous list literal syntax ([10, 20, 30]), Mojo requires an explicit type parameter and construction via .append(). Lists are zero-indexed like Python's, but Mojo does not support negative indices — use len(list) - 1 for the last element instead of list[-1].

List iteration
fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit) for index, fruit in enumerate(fruits): print(index, fruit)
fn main() raises: var fruits = List[String]() fruits.append("apple") fruits.append("banana") fruits.append("cherry") for index in range(len(fruits)): print(fruits[index]) for index in range(len(fruits)): print(index, fruits[index])

Mojo does not yet have a built-in enumerate() equivalent for List. The standard idiom is range(len(collection)) with subscript access. Subscript access (fruits[index]) returns the value directly. Python's for fruit in fruits loop is cleaner — Mojo's syntax is currently more verbose for indexed iteration but gains nothing in ergonomics over index-based access.

Dict basics
scores = {"Alice": 95, "Bob": 87} print(scores["Alice"]) # 95 scores["Charlie"] = 92 print(len(scores)) # 3
fn main() raises: var scores = Dict[String, Int]() scores["Alice"] = 95 scores["Bob"] = 87 print(scores["Alice"]) # 95 scores["Charlie"] = 92 print(len(scores)) # 3

Dict[K, V] is Mojo's typed hash map. Unlike Python's dict literal syntax ({"Alice": 95}), Mojo requires explicit construction and type parameters for both key and value. Subscript access and assignment (dict["key"] = value) work identically to Python. Accessing a missing key raises an error in Mojo — use .get() for safe access with a default value.

Safe dict access
config = {"host": "localhost", "port": "5432"} host = config.get("host", "127.0.0.1") timeout = config.get("timeout", "30") print(host) print(timeout)
fn main() raises: var config = Dict[String, String]() config["host"] = "localhost" config["port"] = "5432" var host = config.get("host", "127.0.0.1") var timeout = config.get("timeout", "30") print(host) print(timeout)

Mojo's Dict.get(key, default) returns the value for key if present, otherwise returns default — exactly the same semantics as Python's dict.get(key, default). This is one of the many areas where Mojo's Python-superset design means existing Python knowledge transfers directly without adjustment.

Dict iteration
grades = {"Alice": "A", "Bob": "B", "Charlie": "C"} for name, grade in grades.items(): print(name, grade)
fn main() raises: var grades = Dict[String, String]() grades["Alice"] = "A" grades["Bob"] = "B" grades["Charlie"] = "C" for key in grades.keys(): var name = String(key) print(name, grades[name])

Python's dict.items() yields key-value pairs in a single loop. Mojo's Dict provides .keys() and .values() iterators, but not .items() in current versions. Iterating over .keys() yields references — copy each key with String(key) before using it as a subscript argument to avoid aliasing errors from the iterator holding a reference into the dict's own memory.

Control Flow
if / elif / else
score = 85 if score >= 90: print("A") elif score >= 80: print("B") elif score >= 70: print("C") else: print("F")
fn main() raises: var score = 85 if score >= 90: print("A") elif score >= 80: print("B") elif score >= 70: print("C") else: print("F")

Mojo's conditional syntax is identical to Python's: if, elif, else, colon-terminated conditions, and indentation-based blocks. Comparison operators (>=, ==, !=, <, >, <=) are the same as Python's. This is a zero-adjustment area for Python programmers moving to Mojo.

for loop with range
for number in range(1, 6): print(number) for number in range(0, 10, 2): print(number)
fn main() raises: for number in range(1, 6): print(number) for number in range(0, 10, 2): print(number)

Mojo's range(start, stop, step) is functionally identical to Python's: stop is exclusive and the three-argument form sets the step. The for number in range(...) syntax is the same in both languages. This example is shown to confirm that Python intuition transfers completely — the code is literally identical.

while loop
countdown = 5 while countdown > 0: print(countdown) countdown -= 1 print("Go!")
fn main() raises: var countdown = 5 while countdown > 0: print(countdown) countdown -= 1 print("Go!")

Mojo's while loop syntax is identical to Python's. The only visible difference in this example is var countdown on the first line — once declared, countdown behaves exactly as in Python. Mojo does not support Python's while/else construct.

break and continue
for number in range(1, 11): if number % 2 == 0: continue if number > 7: break print(number)
fn main() raises: for number in range(1, 11): if number % 2 == 0: continue if number > 7: break print(number)

break and continue behave identically in Python and Mojo — no adjustment needed. This example is shown specifically to confirm that Python loop-control intuition transfers directly. The code is character-for-character identical inside fn main() raises:.

Looping with an index
colors = ["red", "green", "blue"] for index, color in enumerate(colors): print(index, color)
fn main() raises: var colors = List[String]() colors.append("red") colors.append("green") colors.append("blue") for index in range(len(colors)): print(index, colors[index])

Python's enumerate() elegantly yields both the index and value in one expression. Mojo does not have an equivalent of enumerate() for List in current versions — the idiomatic approach is range(len(collection)) with subscript access. This is a place where Python is genuinely more ergonomic; Mojo prioritises compile-time efficiency over iteration convenience.

def Functions
Basic def function
def greet(name: str) -> str: return f"Hello, {name}!" print(greet("Alice"))
def greet(name: String) -> String: return "Hello, " + name + "!" fn main() raises: print(greet("Alice"))

Mojo's def keyword works like Python's: functions are flexible, can raise errors without declaring it, and behave dynamically. Adding type annotations (name: String, -> String) is optional in a def function but provides documentation and enables compile-time checks. The body uses the same return-value semantics as Python — the return keyword is required (unlike Ruby).

Default arguments
def greet(name: str = "World", punctuation: str = "!") -> str: return f"Hello, {name}{punctuation}" print(greet()) print(greet("Alice")) print(greet("Bob", "."))
def greet(name: String = "World", punctuation: String = "!") -> String: return "Hello, " + name + punctuation fn main() raises: print(greet()) print(greet("Alice")) print(greet("Bob", "."))

Default argument syntax is nearly identical between Python and Mojo's def functions — the same param: Type = default form. Unlike Python, Mojo def functions do not support calling arguments by keyword name (e.g., greet(punctuation=".") would not work in current versions) — arguments must be passed positionally.

Multiple return values
def compute_bounds(numbers: list[int]) -> tuple[int, int]: return min(numbers), max(numbers) low, high = compute_bounds([3, 1, 4, 1, 5, 9, 2]) print(low, high)
def find_min_max(first: Int, second: Int, third: Int) -> Tuple[Int, Int]: var minimum = first var maximum = first if second < minimum: minimum = second if second > maximum: maximum = second if third < minimum: minimum = third if third > maximum: maximum = third return (minimum, maximum) fn main() raises: var result = find_min_max(3, 7, 1) print(result[0], result[1])

Python returns multiple values as a tuple that can be destructured with low, high = .... Mojo uses an explicit Tuple[T1, T2] return type and elements are accessed by index (result[0]). Mojo does not yet support automatic tuple destructuring in assignment, so the Python idiom of low, high = find_min_max(...) is not valid syntax.

Recursive functions
def factorial(number: int) -> int: if number <= 1: return 1 return number * factorial(number - 1) print(factorial(6))
def factorial(number: Int) -> Int: if number <= 1: return 1 return number * factorial(number - 1) fn main() raises: print(factorial(6))

Recursive def functions look nearly identical in Python and Mojo. The only change here is the Mojo type name Int (capital I) instead of Python's int. Both languages use the same recursive call syntax. Mojo does not perform tail-call optimisation, and the stack size is limited by the OS rather than Python's configurable recursion limit.

fn Functions
Strict fn functions
# Python type hints are advisory — ignored at runtime def add(x: int, y: int) -> int: return x + y print(add(3, 4)) print(add(1.5, 2.5)) # Works — Python coerces float to int silently? No, it just runs
fn add(left: Int, right: Int) -> Int: return left + right fn main() raises: print(add(3, 4)) # add(1.5, 2.5) would be a compile-time error

Mojo's fn keyword defines a strict function: all parameters and the return type must have explicit type annotations, and the compiler enforces them. Python's type hints on def are advisory and ignored at runtime — a def add(x: int, y: int) still happily accepts a float. An fn function that receives the wrong type is a compile error, enabling zero-cost dispatch without dynamic type checks at runtime.

Mutable parameters (mut)
# Python silently allows functions to mutate mutable arguments def double_in_place(numbers: list[int]) -> None: for index in range(len(numbers)): numbers[index] *= 2 data = [1, 2, 3] double_in_place(data) print(data) # [2, 4, 6]
fn double_in_place(mut numbers: List[Int]): for index in range(len(numbers)): numbers[index] = numbers[index] * 2 fn main() raises: var data = List[Int]() data.append(1) data.append(2) data.append(3) double_in_place(data) for index in range(len(data)): print(data[index])

Python silently allows a function to mutate any mutable object passed to it — there is no indication at the call site or in the signature that mutation will occur. Mojo requires the mut keyword on the parameter to declare that mutation is intended. Without mut, the parameter is borrowed immutably and cannot be modified. A reader can see from the signature whether a function mutates its arguments, making the contract explicit.

Functions that can raise
def parse_age(text: str) -> int: age = int(text) if age < 0: raise ValueError("Age cannot be negative") return age print(parse_age("25"))
fn parse_age(text: String) raises -> Int: var age = atol(text) if age < 0: raise Error("Age cannot be negative") return age fn main() raises: print(parse_age("25"))

In Python, any function can raise any exception at any time — nothing in the signature indicates this. Mojo's fn functions must declare raises in their signature if they can propagate an error. This makes the error contract visible and compiler-checked. atol() is Mojo's built-in for parsing a String to Int — it raises automatically if parsing fails, which is why parse_age must also declare raises.

Function overloading
# Python uses isinstance() checks instead of overloading def describe(value: int | float) -> str: if isinstance(value, int): return f"Integer: {value}" return f"Float: {value}" print(describe(42)) print(describe(3.14))
fn describe(value: Int) -> String: return "Integer: " + String(value) fn describe(value: Float64) -> String: return "Float: " + String(value) fn main() raises: print(describe(42)) print(describe(3.14))

Mojo supports function overloading: multiple fn definitions with the same name but different parameter types. The compiler selects the correct version at compile time based on the argument types. Python achieves similar behaviour by checking isinstance() at runtime — Mojo does the same dispatch statically with no runtime cost and without the branching code in the function body.

Structs vs Classes
Dataclass vs fieldwise_init struct
from dataclasses import dataclass @dataclass class Point: x: float y: float point = Point(3.0, 4.0) print(point.x, point.y)
@fieldwise_init struct Point(Copyable, Movable): var x: Float64 var y: Float64 fn main() raises: var point = Point(3.0, 4.0) print(point.x, point.y)

Python's @dataclass and Mojo's @fieldwise_init both auto-generate a constructor that accepts one argument per field in declaration order. The critical difference is value semantics: assigning a Mojo struct copies it, whereas assigning a Python object copies a reference. Copyable and Movable are built-in traits that must be listed explicitly to enable copying and moving the struct.

Explicit initializer (__init__)
class Rectangle: def __init__(self, width: float, height: float): self.width = width self.height = height rect = Rectangle(3.0, 4.0) print(rect.width)
struct Rectangle: var width: Float64 var height: Float64 fn __init__(out self, width: Float64, height: Float64): self.width = width self.height = height fn main() raises: var rectangle = Rectangle(3.0, 4.0) print(rectangle.width)

Mojo structs use fn __init__(out self, ...) for explicit constructors — the out self parameter indicates that this function initialises the struct rather than receiving a pre-allocated instance. Python's __init__(self, ...) receives an already-allocated object. Both set fields via self.field = value. Mojo field declarations (var width: Float64) must appear explicitly in the struct body before they can be assigned in __init__.

Instance methods
import math class Circle: def __init__(self, radius: float): self.radius = radius def area(self) -> float: return math.pi * self.radius ** 2 circle = Circle(5.0) print(round(circle.area(), 4))
from math import pi struct Circle: var radius: Float64 fn __init__(out self, radius: Float64): self.radius = radius fn area(self) -> Float64: return pi * self.radius * self.radius fn main() raises: var circle = Circle(5.0) print(circle.area())

Read-only methods take self (borrowed, immutable) — matching Python's convention where self refers to the current instance. Mojo imports standard library items explicitly with from math import pi, the same as Python. Methods are called with instance.method() and always require parentheses, even for zero-argument methods. Unlike Python, Mojo does not have a @property decorator equivalent in current versions.

Mutating methods (mut self)
class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 def value(self) -> int: return self.count counter = Counter() counter.increment() counter.increment() print(counter.value()) # 2
struct Counter: var count: Int fn __init__(out self): self.count = 0 fn increment(mut self): self.count += 1 fn value(self) -> Int: return self.count fn main() raises: var counter = Counter() counter.increment() counter.increment() print(counter.value()) # 2

In Python, any method can modify instance attributes — self is always a mutable reference. Mojo requires methods that modify the struct to declare mut self instead of self. Read-only methods use plain self (borrowed). This distinction is enforced by the compiler: calling a mut self method on an immutable binding is a compile error, making mutation intent explicit in the API.

String representation (__str__)
class Person: def __init__(self, name: str, age: int): self.name = name self.age = age def __str__(self) -> str: return f"Person({self.name}, {self.age})" person = Person("Alice", 30) print(person) # __str__ called automatically
struct Person(Stringable): var name: String var age: Int fn __init__(out self, name: String, age: Int): self.name = name self.age = age fn __str__(self) -> String: return "Person(" + self.name + ", " + String(self.age) + ")" fn main() raises: var person = Person("Alice", 30) print(String(person)) # explicit String() conversion required

Python calls __str__ automatically when an object is passed to print(). Mojo requires implementing the Stringable trait (listed in parentheses after the struct name) and then calling String(instance) explicitly to invoke __str__. The method name __str__ is the same in both languages. Mojo's explicit conversion is more verbose but makes it clear that a conversion is happening.

Traits
Defining a trait
from abc import ABC, abstractmethod class Greetable(ABC): @abstractmethod def greet(self) -> str: ...
trait Greetable: fn greet(self) -> String: ... fn main() raises: pass

Mojo's trait keyword is the equivalent of Python's ABC with @abstractmethod. A trait declares required method signatures — structs that list the trait must implement every method, or the compiler rejects the code. Unlike Python ABCs, Mojo traits are checked at compile time with no runtime overhead. Current Mojo versions cannot provide default implementations inside a trait (no equivalent of @classmethod or mixin methods).

Implementing a trait
from abc import ABC, abstractmethod class Describable(ABC): @abstractmethod def describe(self) -> str: ... class Dog(Describable): def __init__(self, name: str): self.name = name def describe(self) -> str: return f"Dog named {self.name}" print(Dog("Rex").describe())
trait Describable: fn describe(self) -> String: ... @fieldwise_init struct Dog(Describable, Copyable, Movable): var name: String fn describe(self) -> String: return "Dog named " + self.name fn main() raises: var dog = Dog("Rex") print(dog.describe())

A Mojo struct implements a trait by listing it in parentheses after the struct name, alongside built-in traits like Copyable and Movable. The compiler verifies that all trait methods are implemented — unlike Python ABCs, where a class can be instantiated with abstract methods still missing and only fail at runtime when those methods are called. Multiple traits are listed together in one set of parentheses.

Generic functions via traits
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: ... class Square(Shape): def __init__(self, side: float): self.side = side def area(self) -> float: return self.side ** 2 class Circle(Shape): def __init__(self, radius: float): self.radius = radius def area(self) -> float: import math return math.pi * self.radius ** 2 for shape in [Square(4.0), Circle(3.0)]: print(round(shape.area(), 4))
from math import pi trait Shape: fn area(self) -> Float64: ... @fieldwise_init struct Square(Shape, Copyable, Movable): var side: Float64 fn area(self) -> Float64: return self.side * self.side @fieldwise_init struct Circle(Shape, Copyable, Movable): var radius: Float64 fn area(self) -> Float64: return pi * self.radius * self.radius fn print_area[ShapeType: Shape](shape: ShapeType): print(shape.area()) fn main() raises: var square = Square(4.0) var circle = Circle(3.0) print_area(square) print_area(circle)

Mojo achieves polymorphism through parametric functions: fn print_area[ShapeType: Shape](shape: ShapeType) accepts any type ShapeType that implements the Shape trait. The square-bracket [ShapeType: Shape] is a compile-time type parameter — the compiler generates a specialised version for each concrete type used, like C++ templates. Python achieves the same result through runtime duck typing. Mojo's approach produces identical behaviour with zero runtime dispatch overhead.

Comparable trait
from functools import total_ordering @total_ordering class Temperature: def __init__(self, celsius: float): self.celsius = celsius def __eq__(self, other) -> bool: return self.celsius == other.celsius def __lt__(self, other) -> bool: return self.celsius < other.celsius boiling = Temperature(100.0) body = Temperature(37.0) print(boiling > body) # True
@fieldwise_init struct Temperature(Copyable, Movable, Comparable): var celsius: Float64 fn __lt__(self, other: Temperature) -> Bool: return self.celsius < other.celsius fn __le__(self, other: Temperature) -> Bool: return self.celsius <= other.celsius fn __gt__(self, other: Temperature) -> Bool: return self.celsius > other.celsius fn __ge__(self, other: Temperature) -> Bool: return self.celsius >= other.celsius fn __eq__(self, other: Temperature) -> Bool: return self.celsius == other.celsius fn __ne__(self, other: Temperature) -> Bool: return self.celsius != other.celsius fn main() raises: var boiling = Temperature(100.0) var body = Temperature(37.0) print(boiling > body) # True

Python's @total_ordering fills in five comparison methods from just __eq__ and one other. Mojo's Comparable trait requires all six comparison dunder methods to be implemented explicitly — there is no automatic derivation. This is more verbose, but each method is straightforward to write and the compiler verifies that none are missing.

Error Handling
Raising errors
def check_positive(number: int) -> int: if number <= 0: raise ValueError("Must be positive") return number print(check_positive(5))
fn check_positive(number: Int) raises -> Int: if number <= 0: raise Error("Must be positive") return number fn main() raises: print(check_positive(5))

Mojo's raise Error("message") is the equivalent of Python's raise ValueError("message"). Unlike Python's rich hierarchy of exception types (ValueError, TypeError, RuntimeError, etc.), Mojo has a single Error type that carries a message string. The function signature must include raises to declare that errors may propagate from this function.

try / except
def risky_divide(dividend: float, divisor: float) -> float: if divisor == 0: raise ZeroDivisionError("Cannot divide by zero") return dividend / divisor try: print(risky_divide(10.0, 2.0)) print(risky_divide(5.0, 0.0)) except ZeroDivisionError as error: print("Caught:", error)
fn risky_divide(dividend: Float64, divisor: Float64) raises -> Float64: if divisor == 0.0: raise Error("Cannot divide by zero") return dividend / divisor fn main() raises: try: print(risky_divide(10.0, 2.0)) print(risky_divide(5.0, 0.0)) except error: print("Caught:", error)

Mojo uses the same try / except syntax as Python. The caught value in except error: is of type Error. Unlike Python, Mojo cannot catch specific error subtypes — there is only one Error type, so except ZeroDivisionError-style filtering is not available. All raised errors are caught by a bare except error: clause.

Propagating errors
def read_config(path: str) -> str: if path != "app.yaml": raise FileNotFoundError(f"File not found: {path}") return "config content" def initialise_app() -> str: config = read_config("app.yaml") # exception propagates automatically return "App initialised with " + config try: print(initialise_app()) except FileNotFoundError as error: print("Setup failed:", error)
fn read_config(path: String) raises -> String: if path != "app.yaml": raise Error("File not found: " + path) return "config content" fn initialise_app() raises -> String: var config = read_config("app.yaml") # error propagates automatically return "App initialised with " + config fn main() raises: try: print(initialise_app()) except error: print("Setup failed:", error)

When a raises function calls another raises function without a try block, the error propagates up the call stack automatically — exactly as Python exceptions propagate through uncaught call frames. Mojo's raises annotation on each function in the chain makes the propagation path compiler-verified: a function that calls a raises function must itself declare raises or wrap the call in try/except.

Errors in struct methods
class BankAccount: def __init__(self, balance: float): self.balance = balance def withdraw(self, amount: float) -> float: if amount > self.balance: raise ValueError("Insufficient funds") self.balance -= amount return self.balance account = BankAccount(100.0) print(account.withdraw(40.0))
struct BankAccount: var balance: Float64 fn __init__(out self, balance: Float64): self.balance = balance fn withdraw(mut self, amount: Float64) raises -> Float64: if amount > self.balance: raise Error("Insufficient funds") self.balance -= amount return self.balance fn main() raises: var account = BankAccount(100.0) print(account.withdraw(40.0))

A Mojo struct method can combine multiple qualifiers: mut self means the method may mutate the struct, and raises means it may propagate an error. Python methods always have implicit mutation access and can raise anything — Mojo makes both properties explicit in the signature. A caller can see at a glance what a method does to state and whether it can fail.

No finally clause
def process(data: str) -> str: if not data: raise ValueError("Empty data") return data.upper() try: result = process("hello") print("Success:", result) except ValueError as error: print("Error:", error) finally: print("Always runs")
fn process(data: String) raises -> String: if len(data) == 0: raise Error("Empty data") return data.upper() fn main() raises: try: var result = process("hello") print("Success:", result) except error: print("Error:", error) # Mojo has no finally clause — use struct destructors instead

Mojo does not have a finally clause or a with statement equivalent in current versions. Cleanup logic that must run regardless of success or failure is managed through Mojo's lifetime system: when a value goes out of scope, its fn __del__(owned self) destructor is called automatically — even if an error propagated. This is the compile-time analogue of Python's context manager protocol.

Performance
SIMD vectors
# Python has no built-in SIMD; this is equivalent serial computation prices = [10.0, 20.0, 30.0, 40.0] discounted = [price * 0.9 for price in prices] print(discounted)
fn main() raises: var prices = SIMD[DType.float64, 4](10.0, 20.0, 30.0, 40.0) var discounted = prices * 0.9 print(discounted)

SIMD[DType, width] is a vector type that maps directly to CPU SIMD instructions. The operation prices * 0.9 applies the discount to all four elements in a single CPU instruction instead of a loop. The width must be a power of two. This is one of Mojo's headline features for ML developers: vectorised math written with familiar operators — no NumPy import required for simple cases, no gap between the code and the machine instructions.

SIMD arithmetic and reduction
# Python serial equivalent vector_a = [1.0, 2.0, 3.0, 4.0] vector_b = [5.0, 6.0, 7.0, 8.0] combined = [a + b for a, b in zip(vector_a, vector_b)] total = sum(combined) print(total) # 36.0
fn main() raises: var vector_a = SIMD[DType.float64, 4](1.0, 2.0, 3.0, 4.0) var vector_b = SIMD[DType.float64, 4](5.0, 6.0, 7.0, 8.0) var combined = vector_a + vector_b var total = combined.reduce_add() print(total) # 36.0

SIMD types support all standard arithmetic operators element-wise: +, -, *, /. The .reduce_add() method sums all elements into a scalar, equivalent to Python's sum(). On modern hardware, the four-element addition is performed in a single CPU instruction. The Python equivalent requires a list comprehension and zip() — clear and readable, but many times slower for large numerical arrays.

Compile-time parameters
# Python determines sizes and types at runtime def sum_batch(values: list[float]) -> float: return sum(values) batch = [10.0, 20.0, 30.0, 40.0] print(sum_batch(batch)) # 100.0
alias BATCH_SIZE: Int = 4 fn sum_batch[batch_size: Int](values: SIMD[DType.float64, batch_size]) -> Float64: return values.reduce_add() fn main() raises: var batch = SIMD[DType.float64, BATCH_SIZE](10.0, 20.0, 30.0, 40.0) print(sum_batch[BATCH_SIZE](batch)) # 100.0

Square-bracket parameters like [batch_size: Int] are resolved at compile time — the compiler generates specialised code for each value of batch_size. Python determines everything at runtime: the list length, the element type, the function dispatch. Mojo's compile-time parameters allow the compiler to emit SIMD instructions of exactly the right width with no runtime branching. alias constants serve as named compile-time values usable as type parameters.

Python interoperability
import numpy as np array = np.array([1.0, 2.0, 3.0, 4.0]) print(array.mean()) # 2.5
from python import Python fn main() raises: var numpy = Python.import_module("numpy") var array = numpy.array([1.0, 2.0, 3.0, 4.0]) print(array.mean()) # 2.5

Mojo can import and call any Python library directly via Python.import_module(). This requires a CPython installation to be available at runtime — it is not available in the Compiler Explorer sandbox, so this example is marked as non-runnable here. In a full Mojo installation, this works seamlessly: you can mix Mojo's performance-critical code with the entire Python ecosystem — NumPy, pandas, scikit-learn, PyTorch — in a single file, gradually replacing hot paths with fn functions and SIMD operations.