Basics: Variables, Types, and Operations

Variables and Mutability

Immutable by Default

In Rust, variables are immutable by default - the opposite of Python!

fn main() {
    let x = 5;
    println!("x = {}", x);
    // x = 6; // ERROR: cannot assign twice to immutable variable
}

Python comparison:

# Python: variables are always mutable
x = 5
x = 6  # OK in Python

Mutable Variables

fn main() {
    let mut x = 5;
    println!("x = {}", x);
    x = 6;  // OK
    println!("x = {}", x);
}

Constants

const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265359;

fn main() {
    println!("Max points: {}", MAX_POINTS);
}

Constants vs Immutable Variables:

Featureconstlet
MutabilityNeverWith mut
Type annotationRequiredOptional
ScopeGlobal or localLocal
Computed atCompile timeRuntime
NamingSCREAMING_SNAKE_CASEsnake_case

Shadowing

Rust allows you to declare a new variable with the same name, which "shadows" the previous one.

fn main() {
    let x = 5;
    let x = x + 1;  // Shadows previous x
    
    {
        let x = x * 2;  // Shadows in inner scope
        println!("Inner x: {}", x);  // 12
    }
    
    println!("Outer x: {}", x);  // 6
}

Python comparison:

# Python just reassigns the variable
x = 5
x = x + 1  # x is now 6 (reassignment, not shadowing)

# Python scoping - to demonstrate shadowing, use different scopes
x = 5
print(f"Outer x: {x}")  # Prints: 5

def inner():
    x = 12  # New local x shadows outer x
    print(f"Inner x: {x}")  # Prints: 12

inner()
print(f"Outer x still: {x}")  # Prints: 5 (unchanged)

Shadowing vs Mutation:

// Shadowing allows type changes
let spaces = "   ";
let spaces = spaces.len();  // OK: now it's a number

// Mutation doesn't
let mut spaces = "   ";
// spaces = spaces.len();  // ERROR: type mismatch

Data Types

Rust is statically typed - all types must be known at compile time. Python is dynamically typed.

Python:

# Type is determined at runtime
x = 5        # x is int
x = "hello"  # x is now str (allowed in Python)

Rust:

// Type is fixed at compile time
let x = 5;           // x is i32 forever
// let x = "hello";  // ERROR: can't change type

Scalar Types

Integers

Rust has multiple integer types with explicit sizes:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Python comparison:

# Python has one int type (arbitrary precision)
x = 5
y = 999999999999999999999  # Python handles this fine
fn main() {
    let a: i32 = -42;           // Signed 32-bit
    let b: u32 = 42;            // Unsigned 32-bit
    let c = 98_222;             // Default: i32
    let d = 0xff;               // Hexadecimal
    let e = 0o77;               // Octal
    let f = 0b1111_0000;        // Binary
    let g = b'A';               // Byte (u8)
}

Integer Overflow:

  • Debug mode: panics
  • Release mode: wraps around
let x: u8 = 255;
// let y = x + 1;  // Panic in debug, wraps to 0 in release

// Explicit wrapping
let y = x.wrapping_add(1);  // Always wraps to 0
let z = x.checked_add(1);   // Returns None if overflow

Floating-Point

fn main() {
    let x = 2.0;        // f64 (default)
    let y: f32 = 3.0;   // f32
}
  • f32: 32-bit, single precision
  • f64: 64-bit, double precision (default, generally faster)

Boolean

fn main() {
    let t = true;
    let f: bool = false;
    
    if t {
        println!("It's true!");
    }
}

Character

fn main() {
    let c = 'z';
    let z: char = 'ℤ';
    let emoji = '😻';
    
    // char is 4 bytes (Unicode scalar)
    println!("Size: {} bytes", std::mem::size_of::<char>());  // 4
}

Note: char is Unicode, not ASCII. It represents a Unicode scalar value.

Compound Types

Tuples

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    
    // Destructuring
    let (x, y, z) = tup;
    println!("y = {}", y);
    
    // Direct access
    println!("First: {}", tup.0);
    println!("Second: {}", tup.1);
}

Unit Type: Empty tuple () represents no value.

fn main() {
    let unit = ();
    // Functions with no return value return ()
}

Arrays

Fixed-size, same type:

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let first = arr[0];
    
    // With type annotation
    let arr: [i32; 5] = [1, 2, 3, 4, 5];
    
    // Initialize with same value
    let arr = [3; 5];  // [3, 3, 3, 3, 3]
    
    // Length
    println!("Length: {}", arr.len());
}

Array vs Vector:

FeatureArrayVector
SizeFixedDynamic
AllocationStackHeap
Type[T; N]Vec<T>

Strings

Two main types in Rust (vs one in Python):

fn main() {
    // String slice (immutable, fixed size)
    let s1: &str = "hello";
    
    // String (mutable, growable)
    let mut s2: String = String::from("hello");
    s2.push_str(", world!");
    
    println!("{}", s1);
    println!("{}", s2);
}

Python comparison:

# Python has one str type (immutable)
s1 = "hello"
s2 = "hello" + ", world!"  # Creates new string

String vs &str:

FeatureString&str
OwnershipOwnedBorrowed
MutabilityCan growFixed size
AllocationHeapUsually stack (or heap)

Type Inference and Annotations

fn main() {
    // Type inferred
    let x = 5;              // i32
    let y = 2.0;            // f64
    let z = true;           // bool
    
    // Explicit annotation
    let a: i32 = 5;
    let b: f64 = 2.0;
    let c: bool = true;
    
    // Sometimes required
    let guess: u32 = "42".parse().expect("Not a number!");
}

Operators

Arithmetic

fn main() {
    let sum = 5 + 10;
    let difference = 95.5 - 4.3;
    let product = 4 * 30;
    let quotient = 56.7 / 32.2;
    let floored = 5 / 2;        // Integer division: 2
    let remainder = 43 % 5;
    
    println!("{} {} {} {} {} {}", 
        sum, difference, product, quotient, floored, remainder);
}

Comparison

fn main() {
    println!("5 == 5: {}", 5 == 5);   // true
    println!("5 != 6: {}", 5 != 6);   // true
    println!("5 > 3: {}", 5 > 3);     // true
    println!("5 < 3: {}", 5 < 3);     // false
    println!("5 >= 5: {}", 5 >= 5);   // true
    println!("5 <= 4: {}", 5 <= 4);   // false
}

Logical

fn main() {
    let t = true;
    let f = false;
    
    println!("AND: {}", t && f);     // false
    println!("OR: {}", t || f);      // true
    println!("NOT: {}", !t);         // false
}

Bitwise

fn main() {
    let a = 0b1010;
    let b = 0b1100;
    
    println!("AND: {:b}", a & b);    // 1000
    println!("OR: {:b}", a | b);     // 1110
    println!("XOR: {:b}", a ^ b);    // 0110
    println!("NOT: {:b}", !a);       // ...11110101
    println!("Left shift: {:b}", a << 1);   // 10100
    println!("Right shift: {:b}", a >> 1);  // 101
}

Compound Assignment

fn main() {
    let mut x = 5;
    x += 1;  // x = x + 1
    x -= 1;  // x = x - 1
    x *= 2;  // x = x * 2
    x /= 2;  // x = x / 2
    x %= 3;  // x = x % 3
}

Type Casting

fn main() {
    let x = 5;
    let y = x as f64;           // i32 -> f64
    
    let a = 65u8;
    let b = a as char;          // u8 -> char ('A')
    
    let c = 1000;
    let d = c as u8;            // Truncates to 232
    
    println!("{} {} {} {}", y, b, c, d);
}

Warning: Casting can truncate or wrap. Use checked conversions when safety matters:

fn main() {
    let x: u32 = 1000;
    
    // Safe conversion
    match u8::try_from(x) {
        Ok(val) => println!("Converted: {}", val),
        Err(e) => println!("Conversion failed: {}", e),
    }
}

String Operations

Creating Strings

fn main() {
    let s1 = String::from("hello");
    let s2 = "world".to_string();
    let s3 = String::new();
}

Concatenation

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from(" world");
    
    // Using +
    let s3 = s1 + &s2;  // s1 is moved, can't use it anymore
    
    // Using format! macro
    let s1 = String::from("Hello");
    let s4 = format!("{} {}", s1, s2);  // s1 and s2 still valid
    
    println!("{}", s4);
}

String Methods

fn main() {
    let mut s = String::from("hello");
    
    // Append
    s.push_str(" world");
    s.push('!');
    
    // Length
    println!("Length: {}", s.len());
    
    // Contains
    println!("Contains 'world': {}", s.contains("world"));
    
    // Replace
    let s2 = s.replace("world", "Rust");
    
    // Split
    for word in s.split_whitespace() {
        println!("{}", word);
    }
    
    // Trim
    let padded = "  hello  ";
    println!("'{}'", padded.trim());
}

String Slicing

fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5];
    let world = &s[6..11];
    let hello2 = &s[..5];   // Same as [0..5]
    let world2 = &s[6..];   // From 6 to end
    let all = &s[..];       // Entire string
    
    println!("{} {}", hello, world);
}

Warning: String slicing uses byte indices, not character indices!

fn main() {
    let s = "😻";
    // let c = &s[0..1];  // PANIC! Invalid UTF-8
    let c = &s[0..4];     // OK: 😻 is 4 bytes
}

Printing and Formatting

println! and format!

fn main() {
    let x = 42;
    let y = 3.14;
    let name = "Alice";
    
    // Basic
    println!("x = {}", x);
    
    // Multiple values
    println!("x = {}, y = {}", x, y);
    
    // Positional
    println!("{0} {1} {0}", "A", "B");  // A B A
    
    // Named
    println!("{name} is {age} years old", name="Bob", age=30);
    
    // Debug output
    let arr = [1, 2, 3];
    println!("{:?}", arr);
    
    // Pretty debug
    println!("{:#?}", arr);
}

Format Specifiers

fn main() {
    let x = 255;
    
    println!("Decimal: {}", x);
    println!("Hex: {:x}", x);
    println!("Hex (upper): {:X}", x);
    println!("Binary: {:b}", x);
    println!("Octal: {:o}", x);
    
    let y = 3.14159;
    println!("Float: {}", y);
    println!("2 decimals: {:.2}", y);
    println!("Scientific: {:e}", y);
    
    let s = "hello";
    println!("Right-align: {:>10}", s);
    println!("Left-align: {:10}", s);
    println!("Center: {:^10}", s);
    println!("Padded: {:0>10}", s);
}

Comments

// Single-line comment

/* 
   Multi-line comment
   Can span multiple lines
*/

/// Documentation comment for the next item
/// These appear in generated docs
fn documented_function() {}

//! Documentation comment for the containing item
//! Used at the top of modules or crates

fn main() {
    // Comments are ignored by compiler
    let x = 5;  // inline comment
}

Practice Exercises

Exercise 1: Temperature Converter

Write a program that:

  • Converts Fahrenheit to Celsius
  • Formula: C = (F - 32) × 5/9
fn main() {
    let fahrenheit = 98.6;
    // Your code here
}

Exercise 2: String Manipulation

Create a program that:

  1. Takes a sentence
  2. Counts the number of words
  3. Reverses each word
  4. Prints the result

Exercise 3: Type Exploration

Write code that:

  1. Declares variables of each scalar type
  2. Prints their sizes using std::mem::size_of::<Type>()
  3. Shows min and max values for integer types

Hint: Use i32::MIN and i32::MAX

Common Mistakes

1. Forgetting mut

let x = 5;
x = 6;  // ERROR

Fix: let mut x = 5;

2. Type Mismatch

let x: i32 = 5;
let y: i64 = x;  // ERROR

Fix: let y: i64 = x as i64;

3. String Concatenation

let s1 = "hello";
let s2 = " world";
let s3 = s1 + s2;  // ERROR: can't add &str

Fix: let s3 = s1.to_string() + s2; or format!("{}{}", s1, s2)

4. Integer Division

let x = 5 / 2;  // Result: 2, not 2.5

Fix: let x = 5.0 / 2.0; or let x = 5 as f64 / 2.0;

Key Takeaways

  • Variables are immutable by default - use mut for mutability
  • Rust has strong static typing with type inference
  • Shadowing allows rebinding and type changes
  • Constants must have type annotations and are computed at compile time
  • Strings come in two flavors: String (owned) and &str (borrowed)
  • Use as for type casting, but be aware of truncation
  • The println! macro supports rich formatting options

Next Steps

In the next chapter, we'll explore Rust's most distinctive feature: the ownership system. This is what makes Rust special and enables memory safety without garbage collection.

Quick Reference

// Variables
let x = 5;              // Immutable
let mut y = 5;          // Mutable
const MAX: u32 = 100;   // Constant

// Types
let a: i32 = 5;         // 32-bit integer
let b: f64 = 3.14;      // 64-bit float
let c: bool = true;     // Boolean
let d: char = '😻';     // Unicode character
let e = (1, 2.0);       // Tuple
let f = [1, 2, 3];      // Array

// Strings
let s1: &str = "hello";           // String slice
let s2: String = String::from("hello");  // String

// Casting
let x = 5;
let y = x as f64;

// Printing
println!("x = {}", x);
println!("Debug: {:?}", arr);