Mojo Type Casting
Type casting converts a value from one data type to another. Mojo requires explicit casting — it never silently converts between incompatible types. This keeps programs predictable and prevents subtle bugs caused by unexpected automatic conversions.
Why Explicit Casting Matters
Implicit casting (Python / JavaScript style): result = 5 + 2.5 → Python automatically widens 5 to 5.0 This hides what is happening and can introduce precision bugs. Explicit casting (Mojo style): result = Float64(5) + 2.5 → you state the intent clearly The compiler knows exactly what types are in play at every step.
Numeric Casting
Cast between numeric types by wrapping the value in the target type's constructor.
fn main():
var i: Int = 42
var f: Float64 = Float64(i) # Int → Float64
print(f) # 42.0
var pi: Float64 = 3.14159
var trunc: Int = Int(pi) # Float64 → Int (truncates, does NOT round)
print(trunc) # 3
Truncation vs Rounding:
Float64(3.9) → Int(3.9) = 3 (drops .9, does NOT round up)
Float64(3.1) → Int(3.1) = 3 (drops .1)
Float64(-2.7)→ Int(-2.7)= -2 (drops .7, moves toward zero)
To round before truncating:
import math
Int(math.round(3.9)) # → 4
Integer Size Casting
fn main():
var big: Int64 = 1_000_000_000
var small: Int32 = Int32(big) # narrow cast — may overflow!
print(small) # 1000000000 (safe here, within Int32 range)
var too_big: Int64 = 3_000_000_000
var overflow: Int32 = Int32(too_big) # wraps around — undefined behavior
print(overflow) # garbage value — do not do this
Safe narrowing check: Int32 max = 2,147,483,647 If your Int64 value > 2,147,483,647, do NOT cast to Int32. Always verify the value fits in the target range before a narrow cast.
Signed and Unsigned Casting
fn main():
var signed: Int8 = -1
var unsigned: UInt8 = UInt8(signed) # -1 in Int8 = 255 in UInt8
print(unsigned) # 255
var u: UInt8 = 200
var s: Int8 = Int8(u) # 200 in UInt8 = -56 in Int8 (two's complement)
print(s) # -56
Two's complement reinterpretation:
Int8 range: -128 to 127
UInt8 range: 0 to 255
Bit pattern 11111111:
As Int8: = -1
As UInt8: = 255
Float Precision Casting
fn main():
var precise: Float64 = 3.141592653589793
var reduced: Float32 = Float32(precise) # loses precision
var half: Float16 = Float16(precise) # loses more precision
print(precise) # 3.141592653589793
print(reduced) # 3.1415927 (Float32 has ~7 significant digits)
print(half) # 3.14 (Float16 has ~3 significant digits)
Precision loss diagram: Float64 ──→ 3.141592653589793 (15 significant digits) Float32 ──→ 3.1415927 ( 7 significant digits) Float16 ──→ 3.14 ( 3 significant digits) Widening (Float16 → Float64): safe, no data lost Narrowing (Float64 → Float16): loses precision — intentional trade-off
Casting to and from String
Numeric → String
fn main():
var age: Int = 28
var score: Float64 = 98.5
var flag: Bool = True
var s1 = String(age) # "28"
var s2 = String(score) # "98.5"
var s3 = String(flag) # "True"
print("Age: " + s1)
print("Score: " + s2)
String → Numeric
fn main() raises:
var text = "42"
var number = Int(text) # "42" → 42
print(number + 1) # 43
var ftext = "3.14"
var fnum = Float64(ftext) # "3.14" → 3.14
print(fnum * 2.0) # 6.28
# Parsing a non-numeric string raises an error:
try:
var bad = Int("hello")
except e:
print("Cannot parse:", str(e))
SIMD Type Casting
SIMD vectors can be cast between element types using the cast() method. This converts each lane independently.
fn main():
var v_int = SIMD[DType.int32, 4](1, 2, 3, 4)
var v_flt = v_int.cast[DType.float32]() # each int → float
print(v_flt) # [1.0, 2.0, 3.0, 4.0]
var v_f64 = SIMD[DType.float64, 4](1.9, 2.7, 3.1, 4.5)
var v_i32 = v_f64.cast[DType.int32]() # truncates each lane
print(v_i32) # [1, 2, 3, 4]
SIMD cast diagram (float → int, width=4):
Input: [1.9, 2.7, 3.1, 4.5]
│ │ │ │
cast cast cast cast (truncate per lane)
│ │ │ │
Output: [ 1, 2, 3, 4 ]
Casting Rules Summary
Conversion | Safety | Data Loss? ------------------------|-------------|--------------------------- Int8 → Int64 | Safe | None (widening) Int64 → Int8 | Risky | Overflow if value too large Float32 → Float64 | Safe | None (widening) Float64 → Float32 | Precision | Decimal digits lost Float64 → Int | Truncates | Fractional part dropped Int → Float64 | Safe | None for most Int values UInt8 → Int8 | Reinterpret | Sign may flip String → Int | Can fail | Raises if not numeric Int → String | Safe | None
Practical Example: Temperature Converter
fn celsius_to_fahrenheit(c: Float64) -> Float64:
return c * 9.0 / 5.0 + 32.0
fn main():
var temps_int = List[Int](0, 20, 37, 100)
for i in range(len(temps_int)):
var c = Float64(temps_int[i]) # Int → Float64 for arithmetic
var f = celsius_to_fahrenheit(c)
var f_int = Int(f) # Float64 → Int to display whole number
print(String(temps_int[i]) + "°C = " + String(f_int) + "°F")
Output:
0°C = 32°F 20°C = 68°F 37°C = 98°F 100°C = 212°F
Key Takeaways
Mojo requires explicit type casting — nothing converts silently. Wrap the value in the target type constructor to cast: Float64(my_int), Int(my_float), String(my_num). Widening casts (small → large type) are always safe. Narrowing casts (large → small) risk overflow or precision loss — verify the value fits first. Float-to-Int truncates toward zero, it does not round. String-to-number conversion can fail at runtime if the string is not a valid number, so wrap it in try/except. SIMD vectors cast all lanes simultaneously with .cast[DType]().
