
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.
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.
- 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}!"); } - 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}"); } - 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()); } - 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}"); } - 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}"); } - 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}"), } } } - 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}"); } - 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()); } - 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:?}"); } - 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:?}"); } - 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()); } - 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}"); } - 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}"); } - 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:?}"); } - 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)); } - 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"), } } - 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}"), } } - 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}"), } } - 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()); } - 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}"); } - 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))); } - 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"])); } - 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)); } - 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.