Mojo Borrowing
Borrowing lets a function temporarily access a value without taking ownership of it. The original owner retains the value after the borrow ends. Mojo enforces strict rules around borrowing to prevent data races and invalid memory access at compile time.
The Borrowing Rules
At any given moment, for a single value, you may have EITHER:
Rule A: Any number of immutable (read-only) borrows
────────────────────────────────────────────
reader1 ──borrow──→ value
reader2 ──borrow──→ value ← both can read simultaneously
reader3 ──borrow──→ value
OR
Rule B: Exactly ONE mutable borrow, and NO other borrows
────────────────────────────────────────────────
writer ──inout──→ value ← sole accessor during this time
Never both A and B at the same time.
This rule guarantees that no two parts of your code disagree about the current state of a value. Python has no such guarantee — concurrent modifications can corrupt data silently.
Immutable Borrows (borrowed)
Pass a value as borrowed to read it without copying or transferring it. Multiple functions can borrow the same value simultaneously.
fn word_count(borrowed text: String) -> Int:
var count = 0
var in_word = False
for i in range(len(text)):
var ch = text[i]
if ch == " " or ch == "\n":
in_word = False
else:
if not in_word:
count += 1
in_word = True
return count
fn char_count(borrowed text: String) -> Int:
return len(text)
fn main():
var essay = "Mojo is fast and safe"
# Both functions borrow 'essay' — no conflicts
print("Words:", word_count(essay)) # Words: 5
print("Chars:", char_count(essay)) # Chars: 21
print(essay) # still valid
Mutable Borrows (inout)
Pass a value as inout to let the function modify it. While the mutable borrow is active, no other borrows of the same value exist.
fn normalize(inout values: List[Float64]):
var total: Float64 = 0.0
for i in range(len(values)):
total += values[i]
if total == 0.0:
return
for i in range(len(values)):
values[i] = values[i] / total
fn main():
var weights = List[Float64](10.0, 20.0, 30.0, 40.0)
normalize(weights)
for i in range(len(weights)):
print(weights[i], end=" ") # 0.1 0.2 0.3 0.4
During normalize() call:
main() ──inout──→ weights (main cannot read/write weights while normalize runs)
↑
sole accessor
After normalize() returns:
main() reclaims ownership of weights
Borrow Checking Example: Compile-Time Safety
The Mojo compiler rejects code that would create conflicting borrows. This prevents entire categories of bugs before the program ever runs.
fn main():
var data = List[Int](1, 2, 3)
# This pattern is safe — sequential borrows, not simultaneous:
var first = data[0] # borrows data briefly, then releases
var second = data[1] # borrows data briefly, then releases
print(first, second) # 1 2
# The compiler prevents simultaneous mutable + immutable borrows:
# You cannot hold an inout reference while also reading the same value
Returning Borrowed References
A function can return a reference into data it borrowed, but the lifetime of that reference must not exceed the lifetime of the original value. Mojo tracks this automatically.
struct DataStore:
var buffer: List[Int]
fn __init__(inout self):
self.buffer = List[Int](10, 20, 30, 40, 50)
fn first(self) -> Int:
return self.buffer[0]
fn last(self) -> Int:
return self.buffer[len(self.buffer) - 1]
fn main():
var store = DataStore()
print(store.first()) # 10
print(store.last()) # 50
Borrow vs Copy — When Each Happens
Situation | What Mojo Does ------------------------------------|--------------------------- fn f(borrowed x: T) | Zero-copy reference fn f(inout x: T) | Zero-copy mutable reference fn f(owned x: T) + caller passes x^ | Move (zero-copy transfer) fn f(owned x: T) + caller passes x | Copy (if T is Copyable) var b = a (small Int, Float64) | Copy var b = a^ (String, List, etc.) | Move
Practical Pattern: Read-Many, Write-Once
struct Config:
var debug: Bool
var max_retries: Int
var timeout: Float64
fn __init__(inout self, debug: Bool, retries: Int, timeout: Float64):
self.debug = debug
self.max_retries = retries
self.timeout = timeout
fn log_config(borrowed cfg: Config):
print("Debug:", cfg.debug)
print("Retries:", cfg.max_retries)
print("Timeout:", cfg.timeout)
fn set_debug(inout cfg: Config, value: Bool):
cfg.debug = value
fn main():
var cfg = Config(False, 3, 30.0)
log_config(cfg) # immutable borrow
set_debug(cfg, True) # mutable borrow
log_config(cfg) # immutable borrow again
Key Takeaways
Borrowing gives temporary access to a value without transferring ownership. Immutable borrows (borrowed) allow multiple simultaneous readers. A mutable borrow (inout) is exclusive — no other borrows exist at the same time. Mojo enforces these rules at compile time, eliminating data races and use-after-free errors. Borrows are zero-cost — no copying or heap allocation happens. The borrow checker is what allows Mojo to be both memory-safe and garbage-collector-free.
