Basics & Syntax
Why Rust?
Section titled “Why Rust?”Rust is a systems programming language designed around three goals:
- Memory safety — the compiler guarantees freedom from null-pointer dereferences, use-after-free bugs, and data races, entirely at compile time.
- Performance — Rust compiles to native machine code with no runtime overhead. There is no garbage collector, no virtual machine, no interpreter.
- Fearless concurrency — the same ownership rules that prevent memory bugs also prevent data races across threads.
The key insight is that Rust achieves these guarantees through the borrow checker — a piece of the compiler that enforces ownership and lifetime rules before your program ever runs. If it compiles, it is memory-safe.
What This Module Covers
Section titled “What This Module Covers”| Lesson | Concept |
|---|---|
| Variables & Mutability | let, mut, shadowing, const |
| Data Types | Scalars, tuples, arrays, type inference |
| Functions | fn, parameters, return types, expressions vs statements |
| Control Flow | if as an expression, loop, while, for, ranges |
A First Rust Program
Section titled “A First Rust Program”Every Rust binary starts from fn main(). The println! macro writes formatted text to standard output. Types are inferred by the compiler when they are unambiguous — you do not need to annotate every variable.
fn main() { println!("Rust guarantees memory safety at compile time."); println!("No garbage collector. No null pointer exceptions."); println!("Zero-cost abstractions. Systems-level performance.");
let x: i32 = 42; let name = "Rust"; println!("x = {}, name = {}", x, name);}The {} in format strings are placeholders — the macro substitutes the corresponding argument value at runtime.
fn main() { println!("Rust guarantees memory safety at compile time."); println!("No garbage collector. No null pointer exceptions."); println!("Zero-cost abstractions. Systems-level performance.");
let x: i32 = 42; let name = "Rust"; println!("x = {}, name = {}", x, name);}Compiling…
How Source Becomes a Running Program
Section titled “How Source Becomes a Running Program”Every lesson in this course leans on one loop: you write source, rustc (via cargo) checks it, and only code that survives every check is allowed to become a binary. There is no separate “linting” step you can skip — type checking and borrow checking are mandatory parts of compilation itself.
flowchart LR S[".rs source"] --> P["rustc: parse + type-check"] P --> B["rustc: borrow-check"] B -->|"rejected"| E["compile error (no binary produced)"] B -->|"accepted"| O["native binary"] O --> R["cargo run / ./binary"]
println! Formatting
Section titled “println! Formatting”println! supports several format specifiers beyond the plain {} used above:
{}calls theDisplaytrait — for human-readable output. Only types that implementDisplaycan go here (most primitives do; custom structs do not, unless you implement it yourself).{:?}calls theDebugtrait — for developer-facing output.#[derive(Debug)]generates this automatically for a struct or enum.{:#?}is the “pretty” variant ofDebug— one field per line, indented.{x}is a captured identifier: instead ofprintln!("{}", x), you can writeprintln!("{x}")directly inside the braces whenxis a variable already in scope.{:>8.2}combines alignment (>right-align), width (8), and precision (.2, two decimal places) — useful for lining up numeric output in columns.
#[derive(Debug)]struct Point { x: i32, y: i32 }
fn main() { let p = Point { x: 3, y: -4 }; println!("{:?}", p); println!("{:#?}", p);
let name = "Rust"; println!("{name} has {} letters", name.len());
let pi = 3.14159; println!("[{:>8.2}]", pi);}What the Compiler Says: {} Requires Display
Section titled “What the Compiler Says: {} Requires Display”{} is not a generic “print anything” placeholder — it specifically requires the value’s type to implement std::fmt::Display. A plain struct has no Display impl by default:
// @expect-error E0277struct Point { x: i32, y: i32 }
fn main() { let p = Point { x: 1, y: 2 }; println!("{}", p);}error[E0277]: `Point` doesn't implement `std::fmt::Display` --> src/main.rs:6:20 |6 | println!("{}", p); | -- ^ `Point` cannot be formatted with the default formatter | | | required by this formatting parameter |help: the trait `std::fmt::Display` is not implemented for `Point` --> src/main.rs:2:1 |2 | struct Point { x: i32, y: i32 } | ^^^^^^^^^^^^ = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.The compiler’s own suggestion is the fix: switch to {:?} and add #[derive(Debug)] to Point. {:?} without deriving Debug fails the same way, with error[E0277]: Point doesn’t implement Debug instead — Debug has to be requested (via #[derive(Debug)]) exactly like Display has to be implemented; neither is free.
Mental Model
Section titled “Mental Model”Think of {} and {:?} as two separate questions you are asking a value: {} asks “how would a user want to see you?” (a trait you opt into deliberately, because it becomes part of your type’s public contract), while {:?} asks “how would a developer want to see you, for debugging?” (a trait you can almost always derive for free). Reaching for #[derive(Debug)] by default and only implementing Display when you actually want a user-facing rendering keeps that distinction honest.
Trade-offs
Section titled “Trade-offs”Rust’s upfront strictness — every value must have a known type, every format placeholder must match a trait the value actually implements — trades a faster “just run it and see” scripting loop for a compiler that catches an entire class of runtime formatting bugs before the program ever starts. The cost is real: cargo build is slower than starting a Python REPL, and beginners spend more time satisfying the compiler up front. The payoff is also real: a Rust binary that compiles cannot crash from TypeError: unsupported format string argument at 3am in production — that class of bug simply cannot reach a compiled binary.
Common Misconceptions
Section titled “Common Misconceptions”- “If it compiles, it has no bugs.” The borrow checker and type checker only rule out memory-safety and type bugs — logic errors (wrong formula, off-by-one in business logic) compile just fine.
- “You have to annotate the type of every variable.” Only
let x: i32 = 42;-style annotations are needed when the compiler cannot infer the type from context or a literal suffix; mostletbindings in real Rust code have no annotation at all. - “
{:?}and{}are interchangeable — just use whichever compiles.” They serve different audiences. Reaching for{:?}on a type that has a meaningfulDisplayimpl (like printing aStringwith{:?}and getting quoted, escaped output) is itself a minor smell worth noticing.
Reading AI-generated code
Section titled “Reading AI-generated code”AI assistants frequently generate println!("{}", some_struct) for a struct they just defined, because it “looks like” the natural placeholder — without checking whether Display exists for that type.
🐛 Spot the bug
#[derive(Debug)]struct Order { id: u32, total: f64 }
fn main() { let order = Order { id: 42, total: 19.99 }; println!("Order: {}", order);}Show the answer
Order derives Debug, not Display — printing it with {} fails to compile with error[E0277]: Order doesn’t implement std::fmt::Display exactly like the Point example above. The fix is println!("Order: {:?}", order) (or {:#?} for a multi-line view), unless the type genuinely needs a hand-written Display impl for user-facing output.