Skip to editor content
learningrust.orglesson 2 of 26

Variables and Data Types

TL;DR: Rust variables are immutable by default — use mut to allow reassignment. Rust is statically typed but infers types automatically. Scalar types include integers (i32, u64), floats (f64), booleans, and characters. All type conversions must be explicit; Rust never coerces types silently. Estimated: 20 minutes.

In this lesson, we'll explore how Rust handles variables and its basic data types. You'll learn about variable declaration, mutability, and the most common data types you'll use in your Rust programs.

Variables and Mutability

In Rust, variables are immutable by default - a key feature that promotes safer code:

fn main() {
    let x = 5; // Immutable by default
    println!("The value of x is: {}", x);
    
    // This would cause a compile error:
    // x = 6; // Cannot assign twice to immutable variable
    
    // To make a variable mutable, use 'mut':
    let mut y = 5;
    println!("The value of y is: {}", y);
    y = 6; // This works!
    println!("The value of y is now: {}", y);
}

Basic Data Types

Rust is a statically typed language, which means it must know the types of all variables at compile time. The compiler can usually infer the type based on how we use the variable.

Scalar Types

Integers

fn main() {
    let a: i32 = 42;        // 32-bit signed integer
    let b: u32 = 42;        // 32-bit unsigned integer
    let c = 42_i64;         // 64-bit signed integer
    let d = 42_u64;         // 64-bit unsigned integer
    
    println!("Different integer types: {}, {}, {}, {}", a, b, c, d);
}

Floating-Point Numbers

fn main() {
    let x = 2.0;      // f64 (default)
    let y: f32 = 3.0; // f32
    
    println!("Floating point numbers: {}, {}", x, y);
}

Boolean

fn main() {
    let t = true;
    let f: bool = false;
    
    println!("Boolean values: {}, {}", t, f);
}

Character

fn main() {
    let c = 'z';
    let z: char = 'ℤ';
    let heart_eyed_cat = '😻';
    
    println!("Characters: {}, {}, {}", c, z, heart_eyed_cat);
}

Type Conversion

Rust requires explicit type conversion in most cases:

fn main() {
    let x = 42_i32;
    
    // Convert i32 to f64
    let y = x as f64;
    
    // String to number conversion
    let str_num = "42";
    let parsed_num = str_num.parse::<i32>().unwrap();
    
    println!("Original: {}", x);
    println!("Converted to float: {}", y);
    println!("Parsed from string: {}", parsed_num);
    
    // Try changing these values and conversions!
}

"Explicit" and "safe" are not the same promise. as is explicit — you have to write it — but it is also a truncating cast: it takes whatever bits fit in the destination and discards the rest, without a warning and without a runtime error. Work out what these three casts produce before you read on.

Predict

Every cast here is written with as, so every one of them compiles. Predict the three printed values before running.

fn main() {
  let big: i32 = 300;
  let as_byte = big as u8; // A

  let f = 3.9_f64;
  let as_int = f as i32; // B

  let neg: i32 = -1;
  let as_unsigned = neg as u8; // C

  println!("{} {} {}", as_byte, as_int, as_unsigned);
}

The answer is 44 3 255. Casting 300 to a u8 keeps only the low eight bits, giving 44; casting -1 to a u8 reinterprets its bit pattern as 255; and casting 3.9 to an i32 chops toward zero to 3 rather than rounding. as is explicit but unchecked — it never warns and never panics. When a value might not fit, reach for .round() before a float cast, or u8::try_from(x) when you want a conversion that can tell you it failed.

Practice Exercises

Try these exercises in the playground above:

  1. Create variables of different numeric types and perform some basic arithmetic
  2. Try converting between different numeric types using the as keyword
  3. Create a string from a number using the to_string() method
  4. Parse a string containing a floating-point number into an f64

Remember: Rust's type system is one of its strongest features, helping catch potential errors at compile time rather than runtime! Once you're comfortable with types, the next big concept is ownership — Rust's most distinctive feature.

Try It Yourself

Reading about conversions is not the same as reaching for the right one. This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out — one widens an integer to a float, one divides in floating point, one parses a string into a number. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each function until every check passes and it prints All checks passed.

The checks are assert_eq! calls inside main. A failing assert_eq! panics and prints both values it compared, so the first failure tells you exactly which function is still a stub and what it should have returned. The three functions reuse only what this lesson taught: the as cast, the fact that operand types decide which arithmetic runs, and .parse::<T>() with .unwrap().

Build

Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide which conversion that function needs, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

// TODO 1: return value widened to an f64, so a later division keeps its
//   fractional part. to_float(7) -> 7.0
fn to_float(value: i32) -> f64 {
  let _ = value;
  0.0
}

// TODO 2: return the average of total over count as an f64 whose
//   fractional part survives. Convert BEFORE dividing.
//   average_of(250, 4) -> 62.5, average_of(7, 2) -> 3.5
fn average_of(total: i32, count: i32) -> f64 {
  let _ = total;
  let _ = count;
  0.0
}

// TODO 3: parse text into an i32. The caller guarantees it holds a valid
//   number, so .unwrap() is acceptable here.
//   parse_score("42") -> 42, parse_score("-7") -> -7
fn parse_score(text: &str) -> i32 {
  let _ = text;
  0
}

// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
  assert_eq!(to_float(7), 7.0, "to_float(7) should be the f64 7.0");
  assert_eq!(average_of(250, 4), 62.5, "average_of(250, 4) should be exactly 62.5, not 62.0");
  assert_eq!(average_of(7, 2), 3.5, "average_of(7, 2) should be exactly 3.5 - the remainder must survive");
  assert_eq!(parse_score("42"), 42, "parse_score('42') should be the number 42");
  assert_eq!(parse_score("-7"), -7, "parse_score('-7') should be the number -7");

  println!("All checks passed.");
  println!("Average of 250 over 4: {}", average_of(250, 4));
  println!("Parsed score: {}", parse_score("42"));
}

Expected output: All checks passed. Average of 250 over 4: 62.5 Parsed score: 42

Once it passes, try two variations and predict each before running:

  1. Convert after dividing instead of before. In average_of, change the body to to_float(total / count) — a single conversion applied to the integer result. Predict which check fails before running. The average_of(250, 4) check fails with left: 62.0, right: 62.5: 250 / 4 runs as integer division, throws the remainder away, and then widens 62 to 62.0. Where the conversion sits relative to the division is the whole difference.
  2. Feed parse_score something that is not a number. Change the final log line to parse_score("42 ") — the same digits with a trailing space. Predict whether it prints 42 or fails before running. It panics: called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }. parse does not trim, and .unwrap() turns that Err into a crash. This does not break a check — it replaces the last echoed line with a panic, which is the honest preview of why Result gets a whole lesson of its own.

Key Takeaways

  • Variables in Rust are immutable by default — use mut when you need to change a value
  • Rust is statically typed: the compiler must know every variable's type at compile time
  • Scalar types include integers (i32, u64), floats (f64), booleans, and characters
  • Type conversion requires explicit casting with as — Rust never converts types implicitly
  • Use .parse::<T>() to convert strings to numbers — it returns a Result, and .unwrap() claims the parse cannot fail (panicking if it does) while ? hands the failure to the caller

Next Steps

You now know how Rust stores and represents data — from integers and floats to booleans, characters, and type conversions. But data alone isn't enough; you need to make decisions and repeat actions based on that data. In the next lesson, we'll cover control flowif expressions, loop/while/for loops, and an early look at match. You'll see how Rust's control flow constructs are expressions that return values, which leads to more concise and idiomatic code.

Next lesson

Control Flow

Master Rust control flow with if expressions, loops, and the match statement for conditional logic and pattern matching

25 min