#set document(title: "1.4 Mojo tips for Python devs", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 1.4#h(0.6em)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: - #strong[Mojo is statically typed.] In Python, types are optional hints that the interpreter #emph[mostly] ignores at runtime. In Mojo, types are first-class. The compiler uses them to generate fast, specialized machine code. - #strong[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. - #strong[Mojo prefers #emph[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. - #strong[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. - #strong[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. #strong[Python]: Nearly everything is mutable. a = 10 \# 10 b = a \# 10 b = b + 10 \# 20 print(a, b) \# 10 20 #strong[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 #strong[Python]: Python's int uses arbitrary precision. It can hold integers as large as memory allows. #strong[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 8 ==== Floating point types #strong[Python]: In Python, float is always 64-bit. #strong[Mojo]: Mojo floating point types #emph[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 #strong[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\] #strong[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. #strong[Python]: counts = {"a": 1, "b": "two"} counts\["c"\] = 3.0 \# {'a': 1, 'b': 'two', 'c': 3.0} #strong[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. #strong[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. #strong[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\] #strong[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. #strong[Python]: def add(a, b): return a + b #strong[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. #strong[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. #strong[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. #strong[Python]: class Point: def \_\_init\_\_(self, x, y): self.x = x self.y = y #strong[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 5 3 Structs deliver performance and predictability. === Types: variable static typing #strong[Python]: In Python, you may assign different types to the same variable. a = "x" \# String a = 10 \# Not an error #strong[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. #strong[Python duck typing]: def sketch(shape): shape.draw() \# Works if shape has draw(), fails at runtime if not #strong[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 #strong["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. #strong["Everything is mutable."]: In Mojo, variables are mutable by default, but function and method arguments may not be. #strong["I can mix types in a list."]: Mojo collections use static types and benefit from the performance that brings. #strong["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. #strong["Classes are the natural way to structure things."]: Mojo's struct value type and its traits offer a better fit for performance-sensitive code. === Use Mojo with AI coding assistants If you're using an AI coding assistant to help translate Python code to Mojo, install Mojo agent skills. The mojo-python-interop skill handles the patterns that trip models up like PythonObject wrapping, import conventions, and type conversions between the two languages. npx skills add modular/skills This installs all four #link("https://mojolang.org/docs/tools/skills")[Mojo agent skills], including mojo-syntax for general language accuracy. === 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.