Rust course
rust programming language icon

Rust Exercises: 24 Practice Problems with Solutions

Rust is learned through the borrow checker. These problems start with bindings, match and iterators, then work through ownership, borrowing, slices, Option and Result, traits, enums and generics.

Each solution is a full program with a main function. Compile it in the editor below — the compiler messages are part of the lesson, so read them rather than skipping to the solution.

Run your answer here

Type your solution, press Run, and use the Input box when a problem asks you to read from standard input.

Rust Compiler
Output
Code runs on the Play with Coding execution engine — your code is saved locally for next time.

How to practise so it actually sticks

Attempt the problem before opening the solution, even if your first version is clumsy. A working ugly answer teaches more than a beautiful one you read. When you get stuck for more than five minutes, read the hint — not the solution — and try again.

After you pass, open the solution and ask what is different about it. Shorter? Fewer variables? A built-in you did not know? That comparison is where most of the growth happens. Then change the problem slightly: sort the other direction, handle an empty input, read a value instead of hard-coding it.

Aim for three to five problems a day rather than thirty in one sitting. Spacing practice over days is what moves syntax from "I can look it up" to "my fingers know it".

The exercises

Showing 24 of 24 exercises.

  1. BeginnerBasics & output

    1. Print a greeting

    Store "Ada" in a variable and print "Hello, Ada!".

    Hint: println! takes {} placeholders.

    fn main() {
        let name = "";
    }
    Show solution
    fn main() {
        let name = "Ada";
        println!("Hello, {name}!");
    }
  2. BeginnerBasics & output

    2. Immutable by default

    Declare a counter, increment it three times and print it — note where mut is required.

    Hint: Bindings are immutable unless you write let mut.

    fn main() {
        let count = 0;
    }
    Show solution
    fn main() {
        let mut count = 0;
        for _ in 0..3 {
            count += 1;
        }
        println!("{count}");
    }
  3. BeginnerBasics & output

    3. Read a line of input

    Read a name from standard input and greet the user. Type a name in the Input box.

    Hint: std::io::stdin().read_line(&mut s) then trim.

    use std::io::stdin;
    
    fn main() {
        
    }
    Show solution
    use std::io::stdin;
    
    fn main() {
        let mut s = String::new();
        stdin().read_line(&mut s).expect("failed to read");
        println!("Hello, {}!", s.trim());
    }
  4. BeginnerConditionals

    4. Even or odd

    Print "even" or "odd" for 17 using if as an expression.

    Hint: let word = if n % 2 == 0 { "even" } else { "odd" };

    fn main() {
        let n = 17;
    }
    Show solution
    fn main() {
        let n = 17;
        let word = if n % 2 == 0 { "even" } else { "odd" };
        println!("{word}");
    }
  5. BeginnerConditionals

    5. match on a value

    Print a grade letter for 84 using match with range patterns.

    Hint: 90..=100 => "A" and so on; match must be exhaustive.

    fn main() {
        let score = 84;
    }
    Show solution
    fn main() {
        let score = 84;
        let grade = match score {
            90..=100 => "A",
            80..=89 => "B",
            70..=79 => "C",
            _ => "F",
        };
        println!("{grade}");
    }
  6. BeginnerLoops

    6. FizzBuzz to 20

    Print 1..=20, replacing multiples of 3 with "Fizz", 5 with "Buzz", both with "FizzBuzz".

    Hint: match (i % 3, i % 5) is an elegant approach.

    fn main() {
        for i in 1..=20 {
        }
    }
    Show solution
    fn main() {
        for i in 1..=20 {
            match (i % 3, i % 5) {
                (0, 0) => println!("FizzBuzz"),
                (0, _) => println!("Fizz"),
                (_, 0) => println!("Buzz"),
                _ => println!("{i}"),
            }
        }
    }
  7. IntermediateLoops

    7. loop with a break value

    Use loop to find the first power of two above 1000 and break with that value.

    Hint: break can return a value out of loop.

    fn main() {
        let mut n = 1;
    }
    Show solution
    fn main() {
        let mut n = 1;
        let first = loop {
            n *= 2;
            if n > 1000 {
                break n;
            }
        };
        println!("{first}");
    }
  8. BeginnerVectors & iterators

    8. Vector statistics

    For vec![4, 9, 1, 7, 3] print the sum, the largest and the smallest.

    Hint: iter().sum::<i32>(), iter().max(), iter().min().

    fn main() {
        let nums = vec![4, 9, 1, 7, 3];
    }
    Show solution
    fn main() {
        let nums = vec![4, 9, 1, 7, 3];
        let sum: i32 = nums.iter().sum();
        println!("{sum} {:?} {:?}", nums.iter().max(), nums.iter().min());
    }
  9. IntermediateVectors & iterators

    9. filter and map

    From 1..=20 keep the even numbers, square them and collect into a Vec.

    Hint: Iterator chains are lazy until you collect.

    fn main() {
        
    }
    Show solution
    fn main() {
        let squares: Vec<i32> = (1..=20).filter(|n| n % 2 == 0).map(|n| n * n).collect();
        println!("{squares:?}");
    }
  10. IntermediateVectors & iterators

    10. Count words with a HashMap

    Count how often each word appears in "to be or not to be".

    Hint: *counts.entry(word).or_insert(0) += 1.

    use std::collections::HashMap;
    
    fn main() {
        
    }
    Show solution
    use std::collections::HashMap;
    
    fn main() {
        let text = "to be or not to be";
        let mut counts: HashMap<&str, i32> = HashMap::new();
        for w in text.split_whitespace() {
            *counts.entry(w).or_insert(0) += 1;
        }
        println!("{counts:?}");
    }
  11. BeginnerStrings

    11. String vs &str

    Build a String by pushing a &str onto it, then print its length and upper case form.

    Hint: push_str appends; to_uppercase returns a new String.

    fn main() {
        
    }
    Show solution
    fn main() {
        let mut s = String::from("Rust");
        s.push_str(" is fast");
        println!("{} {} {}", s, s.len(), s.to_uppercase());
    }
  12. IntermediateStrings

    12. Count vowels

    Count the vowels in "Rust is expressive".

    Hint: chars().filter(|c| "aeiou".contains(*c)).count().

    fn main() {
        let s = "Rust is expressive";
    }
    Show solution
    fn main() {
        let s = "Rust is expressive";
        let n = s.to_lowercase().chars().filter(|c| "aeiou".contains(*c)).count();
        println!("{n}");
    }
  13. IntermediateOwnership & borrowing

    13. Ownership moves

    Pass a String into a function that takes ownership, then fix the code so the caller can still use it.

    Hint: Borrow with &String instead of moving.

    Show solution
    fn shout(s: &String) -> String {
        s.to_uppercase()
    }
    
    fn main() {
        let msg = String::from("borrow me");
        println!("{}", shout(&msg));
        println!("still usable: {msg}");
    }
  14. IntermediateOwnership & borrowing

    14. Mutable borrow

    Write a function that pushes an item into a Vec through a mutable reference.

    Hint: &mut Vec<i32> lets the function modify the caller's vector.

    Show solution
    fn add_item(items: &mut Vec<i32>, value: i32) {
        items.push(value);
    }
    
    fn main() {
        let mut items = vec![1, 2];
        add_item(&mut items, 3);
        println!("{items:?}");
    }
  15. AdvancedOwnership & borrowing

    15. Work with slices

    Write a function taking &[i32] that returns the average, and call it with a Vec.

    Hint: A &Vec<i32> coerces to &[i32] automatically.

    Show solution
    fn average(values: &[i32]) -> f64 {
        if values.is_empty() {
            return 0.0;
        }
        values.iter().sum::<i32>() as f64 / values.len() as f64
    }
    
    fn main() {
        let nums = vec![4, 9, 1, 7, 3];
        println!("{:.2}", average(&nums));
    }
  16. IntermediateOption & Result

    16. Handle an Option

    Find the first number above 5 in a Vec and print it, handling the None case.

    Hint: iter().find(...) returns Option<&i32>; match or use unwrap_or.

    fn main() {
        let nums = vec![1, 3, 8, 2];
    }
    Show solution
    fn main() {
        let nums = vec![1, 3, 8, 2];
        match nums.iter().find(|n| **n > 5) {
            Some(n) => println!("found {n}"),
            None => println!("nothing above 5"),
        }
    }
  17. IntermediateOption & Result

    17. Return a Result

    Write divide(a, b) -> Result<f64, String> refusing zero, and handle both outcomes.

    Hint: Err(String::from("...")) for the failure branch.

    Show solution
    fn divide(a: f64, b: f64) -> Result<f64, String> {
        if b == 0.0 {
            Err(String::from("cannot divide by zero"))
        } else {
            Ok(a / b)
        }
    }
    
    fn main() {
        println!("{:?}", divide(10.0, 4.0));
        match divide(1.0, 0.0) {
            Ok(v) => println!("{v}"),
            Err(e) => println!("error: {e}"),
        }
    }
  18. IntermediateOption & Result

    18. Parse with error handling

    Parse "12x" as i32 and print a friendly message instead of panicking.

    Hint: "12x".parse::<i32>() returns a Result.

    fn main() {
        
    }
    Show solution
    fn main() {
        match "12x".parse::<i32>() {
            Ok(n) => println!("{n}"),
            Err(e) => println!("not a number: {e}"),
        }
    }
  19. IntermediateStructs & traits

    19. Struct with an impl block

    Define Rect with width and height, add new() and area(), then print the area.

    Hint: Associated functions go in impl; new is a convention, not a keyword.

    Show solution
    struct Rect {
        width: f64,
        height: f64,
    }
    
    impl Rect {
        fn new(width: f64, height: f64) -> Self {
            Self { width, height }
        }
        fn area(&self) -> f64 {
            self.width * self.height
        }
    }
    
    fn main() {
        println!("{}", Rect::new(3.0, 4.0).area());
    }
  20. AdvancedStructs & traits

    20. Define and implement a trait

    Define trait Shape with area(), implement it for two types and total their areas through Box<dyn Shape>.

    Hint: Vec<Box<dyn Shape>> stores different shapes together.

    Show solution
    trait Shape {
        fn area(&self) -> f64;
    }
    
    struct Square(f64);
    struct Circle(f64);
    
    impl Shape for Square {
        fn area(&self) -> f64 { self.0 * self.0 }
    }
    impl Shape for Circle {
        fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }
    }
    
    fn main() {
        let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square(3.0)), Box::new(Circle(1.0))];
        let total: f64 = shapes.iter().map(|s| s.area()).sum();
        println!("{total:.2}");
    }
  21. AdvancedStructs & traits

    21. Enum with data

    Model a Shape enum with variants carrying data and compute the area with match.

    Hint: Enum variants can hold named or tuple fields.

    Show solution
    enum Shape {
        Square { side: f64 },
        Circle(f64),
    }
    
    fn area(s: &Shape) -> f64 {
        match s {
            Shape::Square { side } => side * side,
            Shape::Circle(r) => std::f64::consts::PI * r * r,
        }
    }
    
    fn main() {
        println!("{:.2}", area(&Shape::Square { side: 4.0 }));
        println!("{:.2}", area(&Shape::Circle(1.0)));
    }
  22. AdvancedGenerics

    22. A generic function

    Write largest<T: PartialOrd>(items: &[T]) -> &T and use it with numbers and strings.

    Hint: The trait bound is what makes comparison legal.

    Show solution
    fn largest<T: PartialOrd>(items: &[T]) -> &T {
        let mut best = &items[0];
        for item in items {
            if item > best {
                best = item;
            }
        }
        best
    }
    
    fn main() {
        println!("{}", largest(&[3, 9, 2]));
        println!("{}", largest(&["ada", "zoe", "ben"]));
    }
  23. AdvancedAlgorithms

    23. Binary search

    Implement binary search over a sorted slice, returning Option<usize>.

    Hint: Return Some(mid) on a hit and None at the end.

    Show solution
    fn search(a: &[i32], target: i32) -> Option<usize> {
        let (mut lo, mut hi) = (0usize, a.len());
        while lo < hi {
            let mid = (lo + hi) / 2;
            if a[mid] == target {
                return Some(mid);
            } else if a[mid] < target {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        None
    }
    
    fn main() {
        println!("{:?}", search(&[1, 3, 5, 7, 9, 11], 9));
    }
  24. IntermediateAlgorithms

    24. Primes below 50

    Print every prime number below 50.

    Hint: Use a closure or a helper function for the primality test.

    fn main() {
        
    }
    Show solution
    fn main() {
        let is_prime = |n: u32| n > 1 && (2..).take_while(|d| d * d <= n).all(|d| n % d != 0);
        let primes: Vec<u32> = (2..50).filter(|n| is_prime(*n)).collect();
        println!("{primes:?}");
    }

What to do next

If a whole topic feels shaky, go back to that chapter in the 17-chapter Rust course and re-read it, then return here. When the advanced problems feel routine, take the final Rust quiz and claim your certificate, or open the Rust online compiler and build something of your own.