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.
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.
# 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.
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.
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.
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).
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.
# 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.
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.
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().
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.
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.
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.
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.
# 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.
# 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.
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.
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.
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].
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.
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.
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.
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.
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 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.
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.
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:.
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 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).
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.
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.
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.
# 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.
# 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.
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.
# 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.
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.
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__.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
# 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.
# 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.
# 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.
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.