Why Rust?
Lecture 1: Tuesday, May 19, 2026.
Code examples
A common question we get from students is why did we choose to use Rust for this course instead of a different language? Why not continue with Python since it was used in DS 110?
It is important for us to explain our rationale in some detail, and demonstrate to you the motivation behind these decisions. Throughout the course, you will struggle with some Rust-specific concepts and idiosyncrasies. We want you to understand that there is a reason you have to put up with (and overcome) all the hurdles of learning a new language and in dealing with the Rust syntax, compiler, and borrow checker.
Why learn a new programming language? Why not continue to use Python?
Learning a second programming language builds CS fundamentals and teaches you to acquire new languages throughout your career.
Importantly, it also helps you distinguish what is an inherent property of how computers or algorithms work, and what is simply an incidental design decision, implementation choice, or a convention from a particular programming language ( and why that language made these choices, which often has deep and interesting justifications).
Example 1: What do we mean by an incidental choice?
Consider the following Python code:
print('hello world!')
We are all familiar with what this code does. However, nothing in it is particularly insightful about the essence of computers
or programming or even necessary given how they work. The fact that printing uses the word print is incidental. Other languages use
a different word for it, e.g., println in Rust, cout in C++, or console.log in Javascript.
In Python 3, printing is a function call, as evident by the parenthesis following print above. But it does not have to be this way:
in Rust, it is a macro (as we will see later), and even in Python 2, you could print using print "hello world".
Finally, Python does not really distinguish between " and ', but other languages, like Rust, do!
Example 2: Diving deeper into good and bad design choices
Consider the following Python code:
x = 2
y = '3'
print(x + y)
What is the output of running this code?
The above code produces a runtime error when run:
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Is this the result of a fundamental aspect of how computers work? Not really. In fact, in other languages, similar code will have different behaviors.
In Javascript and Java, 2 + "3" results in “23”! These languages implicitly upgrade 2 to a string. Interestingly, in Javascript, 2 * "3" gives 6, but not in Java!
If you were responsible for designing your own language. What alternative behaviors would you consider for
2 + '3'?
Some of your answers:
- “23”
- 5
- an error only if the string is truly a string, e.g.,
2 + 'hello', and 5 otherwise.
Some other answers we think would not be good:
- 0 or the empty string
- 5 for
2 + '3'but2hellofor2 + 'hello' - The number 23 (as a number, not a string).
It seems like there is a range of answers that are acceptable/somewhat reasonable, and a range of clearly unreasonable answers. What are the criteria that distinguish the two?
- Whether the choice reflects our natural/intuitive expectations as humans/programmers
- Whether the choice is confusing or has many corner cases
- Whether the choice results in silent failures if the program is executed that are hard to debug
Example 3: What are the tradeoffs at play when looking at competing choices
Consider the following Python code:
if 10 > 0:
x = 5
print(f"hello world {x}")
The below Rust code is equivalent:
fn main() { if 10 > 0 { let x = 5; println!("hello world {x}"); } }
Running both pieces of code (e.g., using the Rust playground) reveals
that they both produce identical output: hello world 5.
What are the most visible differences between the two code snippets
- The Rust code is contained within a function whose name is
main. - Python uses
:and indentation, Rust uses{and}. - Python delimits statement with new lines, Rust uses
;. - Variable declaration looks different, python does not distinguish from initial variable declaration and changing it in the future, Rust does by requiring initial declaration use
let.
We will understand what (1) and (4) mean later in the course. Let’s focus on (2) and (3).
The following code is valid in Rust, however the same style in Python would not work: (a) the code is not indented properly, and (b) multiple statements share the same line.
fn main() { if 10 > 0 { let x = 5; println!("hello world {x}"); } }
Which do you prefer?
- Some of you prefer Python’s style. Reasons include:
- being easier to read
- looking nicer and shorter (no
mainfunction) - you are already more familiar with Python: that code feels more natural
- Others prefer Rust:
- no issues with indentation (e.g., mixing spaces and tabs): these are a major headache when writing Python code
So, both approaches have nice things (and not so nice things). What dimensions are at odds here?
How nice the code looks and how easy it is to read vs how easy it is to write! E.g., indented code looks nicer
and shorter than code with { and }, but dealing with spaces and tabs is a headache while writing the code.
We call the first readability and the second writability. Sometimes these are subjective, sometimes they are less so.
We will see many more examples later.
Example 4: Mutability — can variables change?
Consider the following Python code:
x = 5
print(x)
x += 1
print(x)
What is the output? Is there anything surprising about this code?
The output is:
5
6
Nothing surprising from a Python perspective: we assigned x to 5, then incremented it. Python lets you reassign any variable at any time without restriction.
Now, consider the equivalent Rust code:
fn main() { let x = 5; println!("{x}"); x += 1; println!("{x}"); }
What do you think happens when we try to compile this?
Unlike Python, this does not compile. Rust gives us the following error:
error[E0384]: cannot assign twice to immutable variable `x`
|
| let x = 5;
| -
| |
| first assignment to `x`
| x += 1;
| ^^^^^^ cannot assign twice to immutable variable
By default, all variables in Rust are immutable1 — once assigned, they cannot be changed.
Definition:
im·mu·ta·ble
adjective
unchanging over time or unable to be changed
If we want a variable to be reassignable, we must explicitly opt into mutability using the mut keyword:
fn main() { let mut x = 5; println!("{x}"); x += 1; println!("{x}"); }
This compiles and produces the same output as the Python version.
Why would Rust make variables immutable by default? What is the tradeoff compared to Python?
One of the most common sources of bugs in large programs is a variable being changed somewhere unexpected — by a far-away part of the code, or inside a function you called. In Python, nothing stops this from happening silently.
By requiring you to explicitly mark variables as mut, Rust forces you to be deliberate about what can change. If something changes that you did not intend to change, the compiler will catch it.
The tradeoff: Python is more convenient (you never have to think about this), while Rust is safer and more explicit — at the cost of having to write mut when you need it.
What about a language where mutability is the default and you explicitly mark variables as immutable — the opposite of Rust?
This is actually how some languages work (e.g., using a const or final keyword). It provides the same safety guarantee in principle: you can still signal that a variable should not change. However, the default matters a lot in practice. Most variables in well-written programs are never reassigned after their initial value — they are used as named constants or intermediate results. If mutability is the default, programmers have to actively remember to mark every such variable, and forgetting means silently missing out on the safety guarantee. Rust’s choice to make immutability the default means safety is what you get for free, and you only pay the cost (writing mut) when you genuinely need to change something.
Okay, you convinced us that we should learn a second language. Why Rust specifically?
Rust is a compiled language! It is very fast! It is also very different from Python!
What about other compiled and fast languages, like C++?
Compared to C++, Rust offers two important advantages:
Memory safety: Rust lets you see how data structures work in memory and manage your own memory, while reducing the risk of making various memory-related mistakes (e.g., a lot of the C/C++ headaches)
Strong type system: Rust has a strong type system that helps programmers writing Rust code ensure their programs are correct, memory safe, and type safe. We will understand what that means in the future.
In other words, the Rust compiler gives you errors and hints about your code to help you write code correctly, rather than allow you to write code any way you want, and then have to deal with fixing it when you encounter various errors during runtime.
Furthermore, Rust is experiencing growing adoption in industry and academia alike. This spans many fields, from low-level systems programming (e.g. the Linux kernel), software engineering, to data science and scientific computing!
We will investigate these notions in more depth later.
Rust, The Rust Compiler, and Speed
A big reason for why we want to teach you a compiled language, and specifically Rust, is that it is fast. But how fast is Rust (or compiled languages in general) you may ask.
Consider our very first, proper code example.
In this example, you can see two equivalent pieces of code in Rust and Python. The code first creates a list with many numbers (10 million numbers!), then sums all these numbers using a simple loop.
In the code, we measure the elapsed time between the start and end of that loop. In other words, the time that it takes to sum up the 10 million numbers.
First, we run the python code using cd src && python3 example.py. On my machine, the sum takes ~310ms. This is impressive! Imagine how long it would take
you, a human, to add up 10 million numbers.
Next, we run the rust code using cargo run. On my machine, the result takes 30ms. That is a 10x improvement! In other words,
if you were a company in the business of adding numbers (which is more or less what all the big AI companies do), you just cut down
your compute costs by a factor of 10! Big savings!
But wait. It gets better. We can ask Rust (specifically, the Rust compiler) to automatically optimize our code as much as it can using
cargo run --release. Now, the result takes ~1.5ms an impressive 200x speedup, or alternatively, a 200x reduction in compute costs.
BIG SAVINGS.
REALLY REALLY BIG SAVINGS.

What is a compiler? Why is it fast?
The content of the python file example.py is human-readable (some may disagree). It contains things like comments, variables with intelligible names, among other things.
Importantly, it is not written in a computer’s native tongue: the ol’ zeros and ones.
Notice how we ran the python code. We first type in the python3 command, then give it the name of the file we want to run. The python3 command is a translator: it is
responsible for reading out the file one line at a time, and translating it to the computer as it goes through it, telling the computer what to do.
This is a lot like when you watch a live TV speech in a foreign language: the TV station super imposes the voice of a live interpreter, that translates every sentence from the speaker’s
language to English, allowing us to understand what the speaker says, but, often at a noticeable delay.
The python3 command does exactly that (which is why it is in fact called the Python interpreter), except the delay it introduces is even more significant (in relative terms), thus causing slower execution of code.
Rust instead does not have an interpreter, it is a compiled language. We can see how that operates using the following commands in our example code above:
# This compiles (or builds) but does not run the code
cargo build
# This takes us to the folder where the compiler produces the compiled code
cd target/debug/
# The compiled code is in a file called example
# This runs it
./example
You can see that our Rust code, once compiled, can be run directly, without any other commands: there are no interpreters!
Furthermore, we can print the contents of the compiled file. Note that it is not human-readable at all! This is all in zeros and ones, in the computer’s native tongue.
# This takes us to the folder where the compiler produces the compiled code
cd target/debug/
# print the content of the compiled file
cat ./example
Finally, cargo run is simply a helpful shortcut that first compiles the code, then runs the compiled code.
Rust lets us get more visibility into how the computer works
Look at our second code example:
rows.rs and columns.rs are almost identical. Both create a large matrix of dimensions 10,000x10,000 (100 million elements) and sum all its elements
while timing the duration required for the sum.
The one difference is that rows.rs goes through the matrix one row at a time, starting from the first row. While the columns.rs goes one column at a time,
starting from the left most column.
Both add the same number of elements (100 million), so it stands to reason that they will take a similar amount of time. Let us see if that is the case:
cargo run --bin rows # runs rows.rs
cargo run --bin columns # runs columns.rs
On my machine, there is a noticeable difference: columns.rs is roughly 3x slower than rows.rs! Why?
It turns out this difference is entirely related to how the computer works, and not the algorithmic nature of either code (which is similar). Specifically, it has to do with how the CPU in a computer fetches the numbers from its memory (i.e., RAM), and how the numbers inside the matrix are placed in that memory. We will look at the details of computer memory and its structure later.
Key takeaway: the important lesson is that we need to understand how the computer works to inform us in how to write the most efficient formulation of our desired program.
-
Change and mutability is a constant in our lives. Unlike in Rust, almost nothing is immutable in reality. If you want to intuitively feel what it means to be immutable or unchanged, listen to this album. ↩