Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust Foundations Practice: Variables, Types & Basic Syntax
Rust57 exercises

Rust Foundations Practice: Variables, Types & Basic Syntax

Practice Rust fundamentals including variables, basic types, constants, shadowing, functions, and control flow with write-from-memory drills.

Cheat SheetCommon ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Declare an immutable variable `x` with the value `5`.

On this page
  1. 1Cheat Sheet: Variable Bindings
  2. 2Compiler Error E0384: Cannot Assign to Immutable Variable
  3. 3Shadowing vs Mutation
  4. 4Statements vs Expressions: The Semicolon Rule
  5. 5Compiler Error E0308: Mismatched Types
  6. 6Data Types: Scalars and Compounds
  7. Scalar Types
  8. Compound Types
  9. 7Compiler Error E0282: Type Annotations Needed
  10. 8const vs let
  11. 9Functions and Return Values
  12. 10Control Flow as Expressions
  13. Unused Variables: the _ Prefix
  14. 11Further Reading
Cheat Sheet: Variable BindingsCompiler Error E0384: Cannot Assign to Immutable VariableShadowing vs MutationStatements vs Expressions: The Semicolon RuleCompiler Error E0308: Mismatched TypesData Types: Scalars and CompoundsCompiler Error E0282: Type Annotations Neededconst vs letFunctions and Return ValuesControl Flow as ExpressionsFurther Reading

You're typing a Rust variable declaration and you pause: is it let mut x or mut let x? You reach for a constant and blank on whether const needs a type annotation. You shadow a variable and the compiler yells about mismatched types.

These moments happen because Rust's foundations have more moving parts than most languages. Variables are immutable by default. Types can be inferred or explicit. Shadowing lets you reuse names while changing types. Small details, but they trip you up every time you haven't written Rust in a week.

This page focuses on the foundational syntax until let, mut, const, and type annotations are automatic.

Related Rust Topics
Rust Ownership & Borrowing: Move Semantics & Borrow RulesRust Structs & Enums: Custom Types, Option & ResultRust Strings: String vs &str, Ownership & MethodsRust Modules & Crates: mod, use, pub, Crate Structure

let is immutable by default, let mut for mutation, const for compile-time constants (type required). Variables are immutable by default so mutation is explicit and intentional.

KeywordMutable?Type annotationEvaluated atScope
let x = 5NoOptional (inferred)RuntimeBlock
let mut x = 5YesOptional (inferred)RuntimeBlock
const X: u32 = 5NeverRequiredCompile timeAny
static X: u32 = 5No (or static mut)RequiredCompile timeGlobal ('static)
let x = x + 1 (shadow)Creates new variableOptionalRuntimeBlock

const values are inlined by the compiler and don't have a guaranteed memory address. static values live at a fixed location for the entire program.

If you're coming from Python, Rust's variable rules work quite differently. See how Python and Rust handle variables for a side-by-side comparison.

Ready to practice?

Start practicing Rust Foundations: Variables, Types & Basic Syntax with spaced repetition

Variables are immutable by default. Add mut when you need to reassign:

// WRONG: let binding is immutable
// let count = 0;
// count += 1;  // error[E0384]

// RIGHT: declare with mut
let mut count = 0;
count += 1;
count += 1;
println!("{count}"); // 2

Shadowing creates a new variable with the same name (can change types). Mutation modifies the existing variable in place (requires mut, cannot change types). Use shadowing for type transformations.

Shadowing creates an entirely new variable that reuses the name. Unlike mut, shadowing can change the type:

// WRONG: mut cannot change the type
// let mut x = "hello";
// x = x.len();  // error[E0308]: expected &str, found usize

// RIGHT: shadow to change type
let x = "hello";
let x = x.len();  // OK: new variable, type is now usize
// Shadowing chain — each let creates a new variable
let x = 5;
let x = x + 1;   // 6
let x = x * 2;   // 12

The last expression in a block (without a semicolon) is its return value. Adding ; turns it into a statement that returns (). This applies to functions, if, match, and closures.

In Rust, the last expression in a block (without a semicolon) is its return value. Adding ; turns it into a statement that returns ():

fn add(a: i32, b: i32) -> i32 {
    a + b   // no semicolon → returns i32
}

// WRONG: semicolon makes it return ()
// fn add(a: i32, b: i32) -> i32 {
//     a + b;  // returns () — error[E0308]: expected i32, found ()
// }

This applies everywhere blocks return values — functions, if expressions, match arms, closures.

if is an expression — both branches must return the same type:

// WRONG: branches return different types
// let status = if ready { "ok" } else { 42 };  // error[E0308]

// RIGHT: same type in both branches
let status = if ready { "ok" } else { "wait" };

This also catches the semicolon mistake (function declares -> i32 but returns ()) and type mismatches in match arms. For a deeper look at how match interacts with Rust's type system, see the pattern matching guide.

Scalar Types

TypeExamplesNotes
Integeri8, i16, i32, i64, i128, isizeSigned. Default: i32
Unsignedu8, u16, u32, u64, u128, usizeusize for indexing
Floatf32, f64Default: f64
Booleanbooltrue / false
Characterchar4 bytes, Unicode scalar value

Numeric literals support type suffixes and visual separators:

let x = 42;          // i32 (default)
let y = 42u64;       // u64 (suffix)
let z = 1_000_000;   // underscores for readability
let hex = 0xff;      // hexadecimal
let bin = 0b1010;    // binary
let byte = b'A';     // u8

Compound Types

// Tuple: fixed-size, mixed types
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;         // destructuring
let first = tup.0;           // index access

// Array: fixed-size, same type
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10];         // [0, 0, 0, ..., 0] (10 elements)
let first = arr[0];          // index access (panics on out-of-bounds)

Most common with parse(), because the compiler cannot guess the target type:

// WRONG: what type should "42" parse into?
// let n = "42".parse().unwrap();  // error[E0282]

// RIGHT: annotate the binding
let n: i32 = "42".parse().unwrap();

// RIGHT: turbofish on the method
let n = "42".parse::<i32>().unwrap();

Also required for const and static declarations (type is never inferred):

const MAX: u32 = 100;        // type required
static GREETING: &str = "hi"; // type required

const is evaluated at compile time. It must have a type annotation, uses SCREAMING_SNAKE_CASE, and can never be mut:

const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;
const GREETING: &str = "hello";

Use const for values fixed across the entire program. Use let for everything else — let bindings are computed at runtime and can depend on function arguments or other variables. Understanding how let and ownership interact is essential; the ownership and borrowing guide covers what happens when you assign or pass values.

Every parameter needs a type annotation. The return type goes after ->:

fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon = return value
}

fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}

if, match, and loop are all expressions — they return values:

let status = if is_ready { "ok" } else { "wait" };

let description = match code {
    200 => "ok",
    404 => "not found",
    _ => "unknown",
};

Loops use iterators for for:

for n in [10, 20, 30] {
    println!("{n}");
}

for i in 0..5 {
    println!("{i}");  // 0, 1, 2, 3, 4
}

let mut count = 0;
while count < 3 {
    count += 1;
}

Once you move beyond scalar types, structs and enums let you define custom compound types that the compiler checks just as rigorously.

Unused Variables: the _ Prefix

Rust warns about unused variables. Prefix with _ to silence the warning:

let _unused = compute_something();  // no warning
let _ = risky_op();                 // discard result (no binding at all)

  • The Rust Book: Common Programming Concepts — variables, types, functions, control flow
  • The Rust Book: Data Types — scalars, tuples, arrays
  • Rust Reference: Items and Expressions — const, static, expression semantics

When to Use Rust Foundations: Variables, Types & Basic Syntax

  • Use `let` for most variables. Immutability prevents accidental changes.
  • Use `let mut` only when you genuinely need to modify the value.
  • Use `const` for compile-time constants that never change.
  • Use type inference when the type is obvious; annotate when it helps readability.
  • Use shadowing to transform a value while keeping the same name.

Check Your Understanding: Rust Foundations: Variables, Types & Basic Syntax

Prompt

What is the difference between shadowing and mutation in Rust?

What a strong answer looks like

Shadowing creates a new variable with the same name (can even change types). Mutation modifies the existing variable in place (requires `mut`). Shadowing uses `let` again; mutation does not.

What You'll Practice: Rust Foundations: Variables, Types & Basic Syntax

Declare immutable variables with letDeclare mutable variables with let mutAdd explicit type annotationsDefine compile-time constants with constUse shadowing to transform valuesUnderstand type inferenceUse numeric type suffixes (i32, u64, f32)Understand the difference between scalar and compound typesWrite basic functions with parameters and return typesUse if expressions and basic loops

Common Rust Foundations: Variables, Types & Basic Syntax Pitfalls

  • You see `cannot assign twice to immutable variable` -- you tried to modify a `let` binding. Add `mut`: `let mut x = 5;`.
  • You shadow a variable and the old value disappears -- shadowing creates a new variable, it does not modify the original. If you need the old value, use a different name.
  • You write `const MAX = 100;` and get `expected type annotation` -- `const` always requires an explicit type. Write `const MAX: u32 = 100;`.
  • Your function returns `()` instead of a value -- you put a semicolon on the last expression. Remove the semicolon: `a + b` not `a + b;`.
  • You get `mismatched types` in an `if` expression -- both branches must return the same type. Check that `if` and `else` produce matching types.
  • You try `let mut x = "hello"; x = x.len();` and get a type error -- `mut` allows changing the value, not the type. Use shadowing with a new `let` instead.

Rust Foundations: Variables, Types & Basic Syntax FAQ

Why are variables immutable by default?

Immutability makes code easier to reason about and enables compiler optimizations. Rust's philosophy is that mutation should be explicit and intentional.

When should I use type annotations?

When the compiler can't infer the type (like with `parse()`), when it improves readability, or when you want to be explicit about the exact type (e.g., `i32` vs `i64`).

What's the difference between const and static?

`const` defines a constant value that is inlined wherever it is used — references to it are not guaranteed to point to the same address. `static` defines a single allocated location that lives for the entire program. Use `const` for compile-time constants, `static` when you need a fixed address or global state.

Can I shadow a variable with a different type?

Yes! Shadowing creates an entirely new variable, so `let x = "5"; let x = x.parse::<i32>().unwrap();` is valid. This is useful for type transformations.

Why use underscores in numbers like 100_000?

Underscores are visual separators that make large numbers readable. The compiler ignores them: `1_000_000` is the same as `1000000`.

Rust Foundations: Variables, Types & Basic Syntax Syntax Quick Reference

Immutable variable
let x = 5;
Mutable variable
let mut count = 0;
count += 1;
Type annotation
let x: i32 = 5;
Constant
const MAX_POINTS: u32 = 100_000;
Shadowing
let x = 5;
let x = x + 1;
let x = x * 2;
Shadowing with type change
let spaces = "   ";
let spaces = spaces.len();
Type suffix
let x = 5i64;
let y = 3.14f32;
Tuple
let tup: (i32, f64, u8) = (500, 6.4, 1);
Array
let arr: [i32; 5] = [1, 2, 3, 4, 5];
Function
fn add(a: i32, b: i32) -> i32 {
    a + b
}
If expression
let is_ready = true;
let status = if is_ready { "ok" } else { "wait" };

Rust Foundations: Variables, Types & Basic Syntax Sample Exercises

Example 1Difficulty: 1/5

Write a minimal Rust program that prints "Hello, world!".

fn main() {
    println!("Hello, world!");
}
Example 2Difficulty: 1/5

Fill in the Rust syntax for a line comment.

//
Example 3Difficulty: 1/5

Add a type annotation to declare x as a 32-bit signed integer.

: i32

+ 54 more exercises

Further Reading

  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read
  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Also in Other Languages

GDScript Foundations Practice

Start practicing Rust Foundations: Variables, Types & Basic Syntax

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.