Pybites Logo Rust Platform

Borrow Checker Patterns

Hard +4 pts

🎯 In Python, you rarely think about data ownership when writing functions. You pass objects around, mutate lists in place, build new strings — the GC handles the rest:

def format_path(parts):
    return "/".join(parts)

def rotate_left(items):
    items.append(items.pop(0))

text.upper()

In Rust, the borrow checker enforces rules about who can read and write data. At first, this feels like fighting the compiler. But experienced Rustaceans have a set of patterns that make borrow checker issues disappear. These patterns aren't workarounds — they produce clearer, safer code.

Pattern 1: Return owned data instead of borrowing

When a function transforms input into something new, return an owned type rather than trying to borrow:

fn to_csv(values: &[i32]) -> String {
    values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(",")
}

In Python, ",".join(str(v) for v in values) returns a new string. The same principle applies in Rust — returning a String instead of &str avoids lifetime complexity. You trade a small allocation for a much simpler signature.

Rule of thumb: if the output is a transformation of the input (not a view into it), return owned data.

Pattern 2: Use .swap() and split_at_mut() for in-place mutation

The borrow checker prevents two mutable references to the same data. So this won't compile:

let first = &mut v[0];
let last = &mut v[v.len() - 1];  // ERROR: can't borrow v again
std::mem::swap(first, last);

Rust provides methods that safely split mutable access:

v.rotate_left(1);  // moves first element to the end

Or for accessing two mutable elements simultaneously:

let (left, right) = v.split_at_mut(mid);
// left and right are non-overlapping — both safely mutable

In Python, items.append(items.pop(0)) rotates easily. In Rust, look at methods on slices like .swap(), .rotate_left(), and .split_at_mut() — each safely handles in-place mutation.

Pattern 3: Clone to break borrow chains

When the borrow checker complains about overlapping borrows, sometimes the simplest fix is to clone:

let name = data.name.clone();  // break the borrow on data
process(&mut data, &name);     // now data can be mutably borrowed

In Python, everything is reference-counted, so sharing is free. In Rust, .clone() has a cost — but it's explicit, and often the clearest solution. Don't optimize away clones until profiling shows they matter.

Pattern 4: Owned wrappers for complex state

When you need to build up data over time — appending to a buffer, accumulating results — use an owned struct:

struct Logger {
    entries: Vec<String>,
}

impl Logger {
    fn log(&mut self, msg: &str) {
        self.entries.push(msg.to_string());
    }
}

The struct owns its Vec, so there are no lifetime parameters. Methods take &mut self to mutate and &self to read. This is the Rust equivalent of a Python class with mutable state — the ownership is just explicit.

Pattern 5: Process and return owned results

When you need multiple derived values from borrowed data, compute them all and return owned results:

fn analyze(data: &[i32]) -> (Vec<i32>, usize) {
    let positives: Vec<i32> = data.iter().filter(|&&x| x > 0).copied().collect();
    let count = positives.len();
    (positives, count)
}

The input is borrowed (&[i32]), but both outputs are owned. No lifetime annotations needed. The function borrows input, processes it, and hands back self-contained results.

Login to see the full task and start coding.

This is a premium exercise

Log in to unlock the full exercise and start coding.

Login to access this exercise