Rust is a compiled, statically typed systems programming language. "Systems programming" means writing software that sits close to the machine: operating systems, browsers, databases, game engines, network proxies, command-line tools and anything else where speed and control over memory matter. For decades that space belonged to C and C++. Rust aims for the same speed and the same low-level control, but with a compiler that refuses to build programs containing whole classes of memory bugs.
This first lesson explains what that promise actually means, how Rust keeps it without a garbage collector, and how to set up a working environment. By the end you will be able to install Rust with rustup, create a project with Cargo, build it in debug and release mode, format it with rustfmt, lint it with Clippy, and read your first compiler error. Interviewers who ask about Rust at this level usually probe three things: why Rust is "memory safe without a garbage collector", the difference between rustc and cargo, and what an edition is. All three are covered here.
What Rust is, in one picture
Every language has to answer one question: who frees memory, and when? There are three broad answers.
| Approach | Languages | Who frees memory | Cost |
|---|---|---|---|
| Manual | C, C++ (raw new/delete) | The programmer calls free or delete | Fast, but forgetting or doubling a free is a bug the compiler does not catch |
| Garbage collector (GC) | Java, Python, JavaScript, Go, C# | A runtime component periodically finds unused objects and frees them | Safe, but adds a runtime, memory overhead and pauses you do not fully control |
| Ownership | Rust | The compiler inserts the free at the exact point a value's single owner goes out of scope | Safe and predictable, but you must follow rules the compiler checks |
A garbage collector is a piece of the language runtime that runs alongside your program, tracks which objects are still reachable and frees the rest. It is convenient, but it means the program carries extra machinery, and the timing of cleanup is decided by the runtime rather than by you.
Rust takes the third road. Every value has exactly one owner (a variable or a field that is responsible for it). When the owner goes out of scope, Rust frees the value automatically, at a point the compiler knows exactly at compile time. Other code can temporarily borrow a value through a reference, and the compiler checks that no borrow outlives the value or conflicts with a change to it. The part of the compiler that enforces this is called the borrow checker. Lessons 4 and 5 cover ownership and borrowing in depth; for now the key idea is that the checks happen at compile time, so they cost nothing when the program runs.
C / C++ Java / Python Rust
+-----------+ +-------------+ +--------------+
| your code | | your code | | your code |
| malloc | | new Obj() | | String::new |
| free <--+-- you +------+------+ +------+-------+
+-----------+ | GC runtime | |
| finds and | compiler inserts
| frees later | drop at scope end
+-------------+
Which bugs does Rust rule out?
Memory safety means a program never reads or writes memory it is not supposed to. In C and C++ the classic violations are:
- Use after free: reading memory after it has been released, possibly after something else has reused it.
- Double free: releasing the same memory twice, which can corrupt the allocator.
- Dangling pointer: a pointer to memory that no longer holds the value it used to.
- Buffer overflow: reading or writing past the end of an array.
- Data race: two threads touching the same memory at the same time, at least one of them writing, with no synchronisation.
In safe Rust (Rust code that does not use the unsafe keyword) the first four cannot happen, and data races are compile errors too. Buffer overflows are prevented by bounds checks: indexing past the end of an array stops the program with a controlled error called a panic instead of silently reading garbage.
What Rust does not promise
Rust does not prevent every bug. Logic errors, deadlocks, integer overflow in release builds (lesson 2), memory leaks and panics are all still possible. The guarantee is specifically about undefined memory behaviour and data races in safe code. Saying this precisely in an interview sounds far more credible than "Rust has no bugs".
Where Rust is used
Rust 1.0 was released in May 2015. Since then it has moved into places where both safety and speed matter:
- Operating systems: the Linux kernel accepts drivers written in Rust, and Android and Windows both ship Rust components.
- Browsers: Firefox's CSS engine is written in Rust; Rust grew out of Mozilla.
- Cloud infrastructure: AWS built the Firecracker micro-VM in Rust, and many proxies, databases and storage engines use it.
- Developer tools: fast command-line tools such as
ripgrep, and JavaScript and Python tooling that is rewritten in Rust for speed. - WebAssembly and embedded: Rust compiles to WebAssembly for browsers and to microcontrollers with no operating system at all.
For a fresher, the practical takeaway is simple: Rust appears in backend, infrastructure and tooling roles, and companies that hire for it expect you to understand ownership and borrowing, not just syntax.
How Rust compares with languages you may know
| Question | Python | Java | C | Rust |
|---|---|---|---|---|
| Compiled ahead of time to machine code? | No (interpreted bytecode) | To JVM bytecode, then JIT | Yes | Yes |
| Types checked when? | At run time | Compile time | Compile time (weakly) | Compile time (strictly) |
| Memory management | GC (reference counting plus cycle collector) | GC | Manual | Ownership, checked by compiler |
| Null references | None can be anywhere | null can be anywhere | NULL can be anywhere | No null; absence is Option<T> (lesson 7) |
| Errors | Exceptions | Exceptions | Return codes | Result<T, E> values (lesson 9) |
| Needs a runtime installed to run the program? | Yes | Yes (JVM) | No | No |
A Rust program compiles to a single native executable. You can copy it to another machine with the same operating system and CPU architecture and run it without installing Rust there.
Installing Rust with rustup
rustup is the official installer and version manager for Rust. It installs the compiler, Cargo, the standard library and the standard tools, and later updates them or switches between versions. You should almost never install Rust from a Linux package manager for learning purposes, because distribution packages often lag behind the current release.
On Linux and macOS, run this in a terminal:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
On Windows, download and run rustup-init.exe from the official site (rustup.rs). Rust on Windows uses the Microsoft C++ build tools as its linker, and the installer offers to set them up if they are missing. The linker is the program that combines compiled pieces into the final executable.
After installation, open a new terminal and check the versions:
rustc --version
cargo --version
This lesson was checked with:
rustc 1.99.0 (b940084d7 2026-09-28)
cargo 1.99.0 (5f94df478 2026-08-27)
The tools that rustup installs by default:
| Tool | What it does |
|---|---|
rustc | The Rust compiler. Turns .rs source files into an executable or library. |
cargo | The build tool and package manager. Calls rustc for you, downloads dependencies, runs tests. |
rustfmt / cargo fmt | Formats code in the official style. |
clippy / cargo clippy | A linter: an extra set of checks that flag code which compiles but is suspicious or unidiomatic. |
rust-std | The precompiled standard library for your platform. |
rust-docs | An offline copy of the official documentation, opened with rustup doc. |
Useful rustup commands:
rustup update # move to the newest stable release
rustup show # which toolchain is active, and why
rustup component add clippy # add a tool if your profile skipped it
rustup doc --book # open the Rust book offline
Release channels
Rust ships a new stable release every six weeks. There are also beta (the next stable, for testing) and nightly (built every night, with unfinished features available behind opt-in flags). For learning, jobs and interviews, use stable. A toolchain is one particular combination of channel and version, such as stable or nightly-2026-10-01.
Interview tip
If asked how Rust keeps the language moving without breaking old code, mention three things: the six-week release train, the strong stability promise (code that compiles on stable keeps compiling on later stables, apart from rare soundness fixes), and editions for the few changes that would break source compatibility.
Your first program with rustc
You can compile a single file directly with rustc. Create hello.rs:
fn main() {
println!("Hello, world!");
}
Compile and run it:
rustc --edition 2024 hello.rs
./hello
Output:
Hello, world!
Line by line:
fn main()declares a function namedmain. Every executable Rust program starts running atmain.- The body sits between
{and}. Rust uses braces, not indentation, to mark blocks, although the standard style indents by four spaces. println!prints a line to standard output. The!means it is a macro, not a function. A macro is code that generates other code at compile time.println!is a macro because it checks the format string against its arguments while compiling, which an ordinary function could not do.- The statement ends with a semicolon.
rustc produced a native executable named hello (or hello.exe on Windows). The --edition 2024 flag tells the compiler which edition of the language to use; more on editions below. Plain rustc is fine for one-file experiments, but real projects use Cargo.
Cargo: the tool you will use every day
Cargo is Rust's build system and package manager in one. Where a Java project might combine javac with Maven or Gradle, and a Python project might combine pip, venv and a test runner, a Rust project uses Cargo for all of it. A reusable library or program published for others is called a crate, and Cargo downloads crates from the public registry at crates.io.
Creating a project
cargo new hello
Output:
Creating binary (application) `hello` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Cargo created this layout and also initialised a Git repository:
hello/
+-- .git/
+-- .gitignore ignores /target
+-- Cargo.toml the manifest: name, version, edition, dependencies
+-- src/
+-- main.rs the binary's entry point (a hello-world main)
The manifest Cargo.toml is written in TOML, a simple configuration format:
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
[package]describes this crate.nameis the crate name and the default executable name.versionfollows semantic versioning (major.minor.patch), which lesson 10 covers.edition = "2024"selects the language edition. New projects get the latest edition automatically.[dependencies]lists the crates this project uses. It starts empty.
Use cargo new name --lib to create a library instead. A library has src/lib.rs instead of src/main.rs and produces code for other crates to use rather than an executable.
Building, running and checking
From inside the hello folder:
cargo build
Output (your project path will differ):
Compiling hello v0.1.0 (/home/you/projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello`
Hello, world!
cargo check
Checking hello v0.1.0 (/home/you/projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
What each command does:
| Command | What happens | When to use it |
|---|---|---|
cargo build | Compiles the project into target/debug/hello | You need the executable but do not want to run it yet |
cargo run | Builds if anything changed, then runs the executable | Day-to-day development |
cargo check | Type-checks and borrow-checks without producing an executable | While writing code; it is noticeably faster than a full build because it skips code generation |
cargo build --release | Compiles with optimisations into target/release/hello | Benchmarks, deployment, sharing the binary |
cargo run --release | Same as above, then runs it | Measuring real performance |
cargo clean | Deletes the target/ folder | Freeing disk space or forcing a full rebuild |
The first build also creates Cargo.lock. This file records the exact versions of every dependency that was resolved, so that every build on every machine uses the same versions. For an application, commit Cargo.lock to version control.
Notice that cargo run printed Finished without a Compiling line: nothing had changed since the last build, so Cargo reused it. Cargo only recompiles what changed.
Habit worth building
Run cargo check constantly while you write code, and cargo run when you want to see output. Most of your time learning Rust is spent reading compiler messages, and cargo check delivers them fastest.
Debug and release builds
Cargo has two built-in profiles (named sets of compiler settings):
| Setting | dev profile (cargo build) | release profile (cargo build --release) |
|---|---|---|
| Output folder | target/debug/ | target/release/ |
| Optimisation level | 0 (none) | 3 (full) |
| Debug information for debuggers | Yes | No |
| Integer overflow checks | On: overflow panics | Off: overflow wraps around |
| Compile speed | Fast | Slower |
| Run speed | Slow, sometimes many times slower | Fast |
Two consequences matter in practice. First, never judge Rust's speed, or compare it with another language, using a debug build. Second, the same program can behave differently on integer overflow in the two profiles; lesson 2 shows this with a real example.
You can tune profiles in Cargo.toml, for example turning overflow checks on in release:
[profile.release]
overflow-checks = true
A slightly bigger program
Replace src/main.rs with:
fn main() {
let name = "Asha";
let marks = [78, 91, 85];
let total: i32 = marks.iter().sum();
println!("Hello, {name}!");
println!("Marks: {:?}", marks);
println!(
"Total: {total}, average: {:.2}",
total as f64 / marks.len() as f64
);
}
Output:
Hello, Asha!
Marks: [78, 91, 85]
Total: 254, average: 84.67
New pieces, each covered properly in later lessons:
letcreates a variable. Variables are immutable by default (lesson 2).[78, 91, 85]is an array, a fixed-size list of values of one type.let total: i32gives the variable an explicit type:i32is a 32-bit signed integer. Elsewhere the compiler infers types, meaning it works them out from how the values are used.marks.iter().sum()walks over the array and adds the values. The type annotation ontotaltellssumwhat type to produce.{name}inside a format string prints the variablenamedirectly.{}takes the next argument.{:?}uses debug formatting, which works for arrays and other compound values that have no single "user-facing" form.{:.2}prints a number with two decimal places.as f64converts an integer into a 64-bit floating-point number. Rust never converts number types silently, so dividing an integer total by a float needs explicit conversions.
Your first compiler errors
Rust's compiler messages are detailed and usually tell you how to fix the problem. Learning to read them is the most important early skill. Every message follows the same shape:
error[E0502]: one-line summary <- error code and message
--> src/main.rs:4:5 <- file:line:column
|
4 | scores.push(40); <- the code involved
| ^^^^^^^^^^^^^^^ explanation <- pointer and label
|
help: a suggested fix <- often copy-pasteable
The code in square brackets, such as E0502, has a long explanation with examples. Run rustc --explain E0502 to read it in your terminal.
Error 1: forgetting the ! on a macro
This does not compile:
fn main() {
println("Hello");
}
error[E0423]: cannot find function `println` in this scope
--> macro_err.rs:2:5
|
2 | println("Hello");
| ^^^^^^^ not found in this scope
|
= note: a macro named `println` exists in another namespace
help: use `!` to invoke the macro
|
2 | println!("Hello");
| +
The compiler looked for a function called println, found only a macro of that name, and suggested the exact fix: add !. Coming from Python's print(...), this is the first error most people hit.
Error 2: the borrow checker catching a real bug
This program looks harmless. It takes a reference to the first element of a growable list (a Vec), adds an element, then prints the reference. This does not compile:
fn main() {
let mut scores = vec![10, 20, 30];
let first = &scores[0];
scores.push(40);
println!("first = {first}");
}
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> safety_err.rs:4:5
|
3 | let first = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(40);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("first = {first}");
| ----- immutable borrow later used here
Why is this a real bug and not the compiler being fussy? A Vec keeps its elements in a block of heap memory. When you push and the block is full, the Vec allocates a bigger block, copies the elements across and frees the old block. If first still pointed into the old block, printing it would be a use-after-free. In C++ the equivalent code (a pointer or reference into a std::vector, then push_back) compiles and may appear to work, until one day it prints garbage or crashes.
before push: scores --> [10][20][30] (full, capacity 3)
^
first
after push: scores --> [10][20][30][40][ ][ ] (new block)
[freed memory]
^
first (would dangle)
Rust's rule is: while something is borrowing a value to read it, nobody may change it. Lesson 5 explains this rule and how to restructure code to satisfy it. Here, the simplest fix is to print first before pushing, or to copy the number out with let first = scores[0];.
Formatting with rustfmt
Rust has one official formatting style, and rustfmt applies it. Teams do not argue about brace placement; they run cargo fmt. Given this messy file:
fn main(){let x=5;
let y =x*2;
if y>5{println!("big: {}",y);}else{println!("small")}
}
Running rustfmt --edition 2024 unformatted.rs (or cargo fmt in a project) rewrites it in place to:
fn main() {
let x = 5;
let y = x * 2;
if y > 5 {
println!("big: {}", y);
} else {
println!("small")
}
}
Use cargo fmt --check in continuous integration (CI): it changes nothing and exits with an error if any file is not formatted, so unformatted code cannot be merged.
Linting with Clippy
The compiler checks that code is valid. Clippy goes further and flags valid code that is likely to be wrong, slow or unidiomatic. It has hundreds of lints (individual checks). Consider this program, which a Java or C programmer might write:
fn main() {
let names = vec!["Ravi", "Meera", "John"];
for i in 0..names.len() {
println!("{}", names[i]);
}
}
It compiles and runs. Running cargo clippy (or clippy-driver --edition 2024 clippy_demo.rs for a single file) reports:
warning: the loop variable `i` is only used to index `names`
--> clippy_demo.rs:3:14
|
3 | for i in 0..names.len() {
| ^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#needless_range_loop
= note: `#[warn(clippy::needless_range_loop)]` on by default
help: consider using an iterator
|
3 - for i in 0..names.len() {
3 + for <item> in &names {
|
warning: useless use of `vec!`
--> clippy_demo.rs:2:17
|
2 | let names = vec!["Ravi", "Meera", "John"];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: you can use an array directly: `["Ravi", "Meera", "John"]`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#useless_vec
= note: `#[warn(clippy::useless_vec)]` on by default
Clippy says two things. The loop indexes into the collection by position even though the position is only used for indexing, so iterating directly is clearer and avoids a bounds check on every access. And the Vec is never resized, so a plain array would do. The idiomatic version:
fn main() {
let names = ["Ravi", "Meera", "John"];
for name in &names {
println!("{name}");
}
}
Output:
Ravi
Meera
John
Idiom
Treat Clippy warnings as free code review. Run cargo clippy before every commit, and in CI use cargo clippy -- -D warnings so that any warning fails the build. When you disagree with a lint for a good reason, silence it locally with an attribute such as #[allow(clippy::needless_range_loop)] and a short comment explaining why.
Editions and the 2024 edition
An edition is a label, set per crate in Cargo.toml, that selects a version of the language's surface rules. Rust has had four: 2015, 2018, 2021 and 2024. The 2024 edition became stable with Rust 1.85 in February 2025, and it is the default for new projects.
Editions exist to make the few changes that would otherwise break existing source code, such as reserving a new keyword. The key properties:
- Opt-in per crate. An old crate keeps compiling under its old edition with a new compiler. Nothing changes until you edit the
editionfield. - Interoperable. A 2024-edition crate can depend on a 2015-edition crate and the other way round. All editions compile to the same internal representation.
- Small. Editions change surface details, not the core language. Ownership, borrowing and the standard library work the same way in every edition.
- Automated migration.
cargo fix --editionrewrites most code for you when you move to a newer edition.
Examples of what the 2024 edition changed, for awareness rather than memorisation:
| Change | What it means |
|---|---|
gen is a reserved keyword | Kept free for future generator syntax |
extern blocks must be written unsafe extern | Makes it visible that declaring foreign functions is an unsafe promise |
Some attributes are written #[unsafe(no_mangle)] | Marks attributes that can cause undefined behaviour if misused |
std::env::set_var is an unsafe function | Changing environment variables while other threads read them is not thread safe on some platforms |
Temporaries in if let conditions and block tail expressions are dropped earlier | Fixes some surprising lock-holding and borrow errors |
Future and IntoFuture are in the prelude | Async code needs fewer imports (lesson 17) |
The prelude is the small set of standard-library names (such as String, Vec, Option) that every Rust file can use without importing them.
Common misconception
An edition is not a version of the compiler. Rust 1.99 can compile 2015, 2018, 2021 and 2024 edition code. "Which Rust version do you use?" and "which edition?" are different questions: the version is the toolchain (for example 1.99.0), the edition is a per-crate setting.
A tour of what makes Rust different
Before the detailed lessons, here is a map of the ideas you will meet, with the lesson that covers each. Knowing the shape of the language helps you see why early rules exist.
| Idea | Short description | Lesson |
|---|---|---|
| Immutability by default | Variables cannot change unless declared mut | 2 |
| Expressions everywhere | if, match and blocks produce values | 3 |
| Ownership | Each value has one owner; it is freed when the owner goes away | 4 |
| Borrowing | References that the compiler proves are always valid | 5 |
Enums with data and match | Model states precisely; the compiler checks every case is handled | 7 |
Option and Result | No null, no exceptions: absence and failure are ordinary values | 7, 9 |
| Traits and generics | Shared behaviour and reusable code with no runtime cost | 11, 12 |
| Fearless concurrency | Data races are compile errors | 16 |
Zero-cost abstraction is a phrase you will hear often. It means that high-level features such as iterators, generics and closures compile to code as fast as the hand-written low-level version, so you do not pay at run time for writing clearer code.
Common beginner pitfalls
Pitfall: benchmarking a debug build
cargo run builds without optimisations. A loop that takes seconds in debug may take milliseconds with cargo run --release. Always use release builds when you measure or compare speed.
Pitfall: fighting the compiler instead of reading it
Beginners often change code at random until an error disappears. Instead, read the whole message: the labels under the ^^^ markers tell a story ("borrow occurs here", "mutable borrow occurs here", "borrow later used here"), and the help: line is frequently the exact fix.
Pitfall: mixing up rustc and cargo
Running rustc src/main.rs inside a Cargo project ignores Cargo.toml, so it uses a default edition and none of your dependencies. Inside a project, always go through cargo.
Exercises
Exercise 1: say hello properly
Create a new Cargo project called intro. Make it print your name and the language you are learning on one line, using two variables and inline {name} formatting.
Solution
Run cargo new intro, then cd intro, put this in src/main.rs and run cargo run:
fn main() {
let name = "Deepa";
let language = "Rust";
println!("My name is {name} and I am learning {language}.");
}
My name is Deepa and I am learning Rust.
Exercise 2: temperature conversion
Write a program that converts 37.5 degrees Celsius to Fahrenheit using F = C * 9 / 5 + 32 and prints both values.
Solution
Use floating-point literals (9.0, not 9) throughout, because Rust will not mix integers and floats in one expression.
fn main() {
let celsius = 37.5;
let fahrenheit = celsius * 9.0 / 5.0 + 32.0;
println!("{celsius} C = {fahrenheit} F");
}
37.5 C = 99.5 F
Exercise 3: read an error, fix the code
The program in "Error 1" above fails to compile. Without looking at the help line, explain what the ! means and fix the program.
Solution
println! is a macro, and macros are invoked with !. Macros run at compile time and can check the format string against the arguments, which is why printing is a macro rather than a function.
fn main() {
println!("Hello");
}
Hello
Exercise 4: listen to Clippy
Run cargo clippy on the indexing loop from the Clippy section and rewrite the program until Clippy reports nothing.
Solution
Iterate over the collection directly instead of over its indices, and use an array because the list never grows. The fixed program is the one shown at the end of the Clippy section; with it, cargo clippy finishes with no warnings.
Exercise 5: debug versus release
In your intro project, run cargo build and then cargo build --release. Where does each executable end up, and which profile checks integer overflow?
Solution
The debug build goes to target/debug/intro and the release build to target/release/intro (with .exe on Windows). The dev profile used by cargo build turns overflow checks on, so overflow panics; the release profile turns them off, so overflow wraps around unless you set overflow-checks = true under [profile.release].
Interview questions
Q1. What does "memory safe without a garbage collector" mean in Rust?
It means safe Rust code cannot have use-after-free, double free, dangling pointers or out-of-bounds access, yet there is no runtime component that scans memory to free it. Instead, every value has a single owner and is freed when that owner goes out of scope, and the compiler's borrow checker proves that every reference is valid for as long as it is used. Because the checks happen at compile time, they add no run-time cost.
Q2. What is the difference between rustc and cargo?
rustc is the compiler: it turns Rust source into an executable or library. Cargo is the build tool and package manager that calls rustc with the right flags, reads Cargo.toml, downloads and builds dependencies, and runs tests, benchmarks, formatting and linting. In practice you use Cargo for everything and rustc directly only for single-file experiments.
Q3. What is the difference between cargo build, cargo check and cargo run?
cargo check runs the compiler's analysis (type checking and borrow checking) but skips generating machine code, so it is the fastest way to find errors. cargo build does the full compilation and writes an executable into target/debug. cargo run builds if needed and then runs the executable.
Q4. How do debug and release builds differ?
The debug (dev) profile does no optimisation, includes debug information and enables integer overflow checks, so it compiles fast and catches overflow with a panic. The release profile optimises fully (level 3) and disables overflow checks, so arithmetic wraps on overflow. Release builds can be many times faster, so performance should only ever be measured with --release.
Q5. What is Cargo.lock and should you commit it?
Cargo.lock records the exact version of every dependency Cargo resolved, so builds are reproducible across machines and over time. For applications (binaries) you should commit it. For libraries, the current Cargo guidance also allows committing it, but downstream users resolve their own versions regardless, because only the top-level project's lock file is used.
Q6. What is an edition, and how is it different from a Rust version?
An edition (2015, 2018, 2021, 2024) is a per-crate setting that selects the language's surface rules, used to introduce changes that would break source compatibility, such as new keywords. A version such as 1.99.0 is a release of the toolchain, and one toolchain compiles all editions. Crates on different editions link together without problems.
Q7. Why is println! a macro and not a function?
Rust functions have a fixed number of parameters with fixed types, but println! accepts any number of arguments of different types. As a macro it expands at compile time and checks that the format string's placeholders match the arguments, so a missing argument or a value that cannot be formatted is a compile error rather than a runtime surprise.
Q8. What are rustfmt and Clippy, and how would you use them in a team?
rustfmt formats code into the single official style, and Clippy is a linter with hundreds of checks for code that compiles but is suspicious or unidiomatic. A typical team runs cargo fmt --check and cargo clippy -- -D warnings in CI so unformatted code or new warnings fail the build. This removes style debates from code review.
Q9. Does Rust prevent all bugs? Give examples of what it does not prevent.
No. Safe Rust prevents memory-safety violations and data races, but it still allows logic errors, deadlocks, memory leaks (for example with reference-count cycles), panics such as out-of-bounds indexing or unwrap on an error, and integer overflow wrapping in release builds. Code inside unsafe blocks can also break the guarantees if its author makes a mistake.
Q10. How does Rust keep bounds safety for arrays without a garbage collector?
Indexing with a[i] checks i against the length at run time and panics if it is out of range, instead of reading arbitrary memory. Where possible the compiler removes the check, for example when iterating with a for loop over the collection itself. Methods such as get return an Option so you can handle a missing index without a panic.
Key takeaways
- Rust is a compiled systems language that matches C and C++ for control and speed while ruling out memory-safety bugs in safe code.
- Memory is freed when a value's single owner goes out of scope; the borrow checker proves references are valid at compile time, so there is no garbage collector.
- Install and update Rust with
rustup; use the stable channel. - Use Cargo for everything:
cargo new,cargo check,cargo build,cargo run,cargo build --release. - Debug builds are unoptimised and check integer overflow; release builds are optimised and wrap on overflow.
- Run
cargo fmtandcargo clippyroutinely, and treat compiler messages, including theirhelp:lines, as instructions. - An edition (2024 for new code) is a per-crate setting, not a compiler version, and crates of different editions work together.
Next lesson
Continue with Variables and types in Rust.

