Skip to content
Learn Rust
Theme

Rust for Python developers

A lot of Python carries over to Rust, and a few habits do not. This page lists ten of those habits. Each one was run twice in the same image, as a Python program and as a Rust program, so the outputs and compiler errors below are real ones. It summarises the lessons and links to them; it does not replace them.

the image behind this page
$ python3 --version
Python 3.13.5
$ rustc --version
rustc 1.99.1 (58cf8f9c3 2026-10-13)
$ cargo --version
cargo 1.99.1 (5f94df478 2026-08-27)

RememberPython finds many mistakes when the line runs, if it ever does, unless you also run a checker. Rust asks for the type and the owner up front, so the compiler refuses several of the ten habits below before the program starts.

  • Control flow reads the same. for x in items, if/else, and a match that is close to Python 3.10’s (Match).
  • Unpacking and formatting. let (lo, hi) = pair; is lo, hi = pair, and println!("{name} is {age}") is an f-string (Patterns, Macros, attributes and naming).
  • Lazy iterator chains work like generator expressions (habit 7).
  • @dataclass and with have counterparts: a struct with #[derive(...)] (Structs), and cleanup at the end of a scope (the last row of the table).
  • async def and await have a similar shape (Rust writes .await after the call), and a coroutine and a future are both lazy (habit 10).

Your habit: write count: int = "five", or call a function marked int with strings. Python runs it. The typing docs say why: “The Python runtime does not enforce function and variable type annotations.”

Pythonh1_types.py
def add(a: int, b: int) -> int:
return a + b
count: int = "five"
print(add("ab", "cd"))
print(count)
$ python3 h1_types.py
abcd
five
Rusth1_types.rs
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let count: i32 = "five";
println!("{}", add("ab", "cd"));
println!("{count}");
}
$ cargo run
error[E0308]: mismatched types
 --> src/main.rs:6:22
  |
6 |     let count: i32 = "five";
  |                ---   ^^^^^^ expected `i32`, found `&str`
  |                |
  |                expected due to this

error[E0308]: arguments to this function are incorrect
 --> src/main.rs:7:20
  |
7 |     println!("{}", add("ab", "cd"));
  |                    ^^^ ----  ---- expected `i32`, found `&str`
  |                        |
  |                        expected `i32`, found `&str`
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |    ^^^ ------  ------

A checker such as mypy does read the hints, and finds both mistakes before the run (the steps to install it are in the toolchain map below; they need network):

terminal
$ .venv/bin/pip install pytest==9.1.1 mypy==2.4.0 ruff==0.17.2   # needs network; their own dependencies are not pinned
$ .venv/bin/mypy h1_types.py
h1_types.py:5: error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
h1_types.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int"  [arg-type]
h1_types.py:6: error: Argument 2 to "add" has incompatible type "str"; expected "int"  [arg-type]
Found 3 errors in 1 file (checked 1 source file)

In Rust the compiler is the checker, and it builds nothing when it reports an error, so the program does not run at all. Declarations and types covers the syntax. Python’s int has no fixed size, where Rust asks you to pick one (Big numbers are not built in).

Your habit: b = a and then b.append(4), or a function that appends to the list you passed. Both change a. In Python each name leads to an object, and nothing in the def line says a function will change its argument.

Pythonh2_alias.py
a = [1, 2, 3]
b = a
b.append(4)
print(a)
def add_bonus(scores):
scores.append(100)
add_bonus(a)
print(a)
$ python3 h2_alias.py
[1, 2, 3, 4]
[1, 2, 3, 4, 100]
Rusth2_alias.rs
fn main() {
let a = vec![1, 2, 3];
let mut b = a;
b.push(4);
println!("{a:?}");
}
$ cargo run
error[E0382]: borrow of moved value: `a`
 --> src/main.rs:5:16
  |
2 |     let a = vec![1, 2, 3];
  |         - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait
3 |     let mut b = a;
  |                 - value moved here
4 |     b.push(4);
5 |     println!("{a:?}");
  |                ^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let mut b = a.clone();
  |                  ++++++++

In Rust let mut b = a; moves the vector into b, so using a afterwards is E0382. You pick what you meant: a.clone() for an independent copy, or lend the list with &mut so a function can change it. The signature says so, and so does the call:

Rusth2_fixed.rs
fn add_bonus(scores: &mut Vec<i32>) {
scores.push(100);
}
fn main() {
let mut a = vec![1, 2, 3];
let mut b = a.clone();
b.push(4);
println!("{a:?} {b:?}");
add_bonus(&mut a);
println!("{a:?}");
}
$ cargo run
[1, 2, 3] [1, 2, 3, 4]
[1, 2, 3, 100]

Read Ownership, Borrowing and Mutable borrows, and Copy or reference?, which has Python detectors for “is this a copy?”.

3. Default arguments, and the mutable default

Section titled “3. Default arguments, and the mutable default”

Your habit: def add_item(item, bucket=[]). The default list is made once, when def runs, so the second call sees the first call’s item. The Python reference says the default “is evaluated once, when the function is defined, and that the same ‘pre-computed’ value is used for each call”, and suggests None and a test, which is the second function below.

Pythonh3_default.py
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item("a"))
print(add_item("b"))
def add_item_fixed(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add_item_fixed("a"))
print(add_item_fixed("b"))
$ python3 h3_default.py
['a']
['a', 'b']
['a']
['b']
Rusth3_default.rs
fn add_item(item: &str, bucket: Option<Vec<String>>) -> Vec<String> {
let mut bucket = bucket.unwrap_or_default();
bucket.push(item.to_string());
bucket
}
fn main() {
println!("{:?}", add_item("a", None));
println!("{:?}", add_item("b", None));
}
$ cargo run
["a"]
["b"]

Rust has no default arguments, so there is no default value to share between calls. A call that leaves the argument out is an error, and None builds a fresh vector inside the call:

$ cargo run
error[E0061]: this function takes 2 arguments but 1 argument was supplied
 --> src/main.rs:8:22
  |
8 |     println!("{:?}", add_item("a"));
  |                      ^^^^^^^^----- argument #2 of type `Option<Vec<String>>` is missing
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn add_item(item: &str, bucket: Option<Vec<String>>) -> Vec<String> {
  |    ^^^^^^^^             ---------------------------
help: provide the argument
  |
8 |     println!("{:?}", add_item("a", /* Option<Vec<String>> */));
  |                                  +++++++++++++++++++++++++++

The struct forms (Default, a literal ending in ..Default::default(), a builder) are in Constructors, overloading and default arguments.

Your habit: let int(text) raise and catch ValueError somewhere above. Nothing in def parse_port(text) tells a caller it can raise.

Pythonh4_errors.py
def parse_port(text):
return int(text)
for text in ["8080", "http"]:
try:
print(parse_port(text))
except ValueError as e:
print("bad port:", e)
$ python3 h4_errors.py
8080
bad port: invalid literal for int() with base 10: 'http'
Rusth4_errors.rs
use std::num::ParseIntError;
fn parse_port(text: &str) -> Result<u16, ParseIntError> {
text.parse()
}
fn main() {
for text in ["8080", "http"] {
match parse_port(text) {
Ok(port) => println!("{port}"),
Err(e) => println!("bad port: {e}"),
}
}
}
$ cargo run
8080
bad port: invalid digit found in string

In Rust the failure is in the signature: Result<u16, ParseIntError>. You handle it with match, pass it up with ?, or stop with unwrap/expect, which panic. Discarding a Result is a compiler warning (Do not ignore a Result). There is no finally: cleanup is Drop (Cleanup still happens). See Errors.

Your habit: x or default, if items: and d.get(key).upper(). The run shows two traps. 0 or 5 is 5 even though 0 was a real value, and get("bob") returned None, which has no upper, found when that line ran.

Pythonh5_none.py
users = {"ada": "Ada Lovelace"}
count = 0
print(count or 5)
print(users.get("bob").upper())
$ python3 h5_none.py
5
Traceback (most recent call last):
  File "/home/you/habits/h5_none.py", line 4, in <module>
    print(users.get("bob").upper())
          ^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'upper'
Rusth5_none.rs
use std::collections::HashMap;
fn main() {
let mut users = HashMap::new();
users.insert("ada", "Ada Lovelace");
println!("{}", users.get("bob").to_uppercase());
}
$ cargo run
error[E0599]: no method named `to_uppercase` found for enum `Option<T>` in the current scope
 --> src/main.rs:6:37
  |
6 |     println!("{}", users.get("bob").to_uppercase());
  |                                     ^^^^^^^^^^^^ method not found in `Option<&&str>`
  |
help: consider using `Option::expect` to unwrap the `&&str` value, panicking if the value is an `Option::None`
  |
6 |     println!("{}", users.get("bob").expect("REASON").to_uppercase());
  |                                    +++++++++++++++++

  ... (notes pointing into the standard library trimmed)

get returns an Option, a different type from the text inside it, so the call does not compile until you say what happens for None: match, map, or unwrap_or(5), which replaces only None. An if takes only a bool, so if items: is if !items.is_empty() (rows in the table below). See Option and Only a bool is a condition.

Your habit: put any objects with an area() method in a list. Python looks the method up by name when the call runs, so the list builds, 4 prints, and Rock fails afterwards.

Pythonh6_duck.py
class Square:
def area(self):
return 4
class Rock:
pass
shapes = [Square(), Rock()]
for shape in shapes:
print(shape.area())
$ python3 h6_duck.py
4
Traceback (most recent call last):
  File "/home/you/habits/h6_duck.py", line 12, in <module>
    print(shape.area())
          ^^^^^^^^^^
AttributeError: 'Rock' object has no attribute 'area'
Rusth6_duck.rs
trait Shape {
fn area(&self) -> u32;
}
struct Square;
impl Shape for Square {
fn area(&self) -> u32 {
4
}
}
struct Rock;
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square), Box::new(Rock)];
for shape in &shapes {
println!("{}", shape.area());
}
}
$ cargo run
error[E0277]: the trait bound `Rock: Shape` is not satisfied
  --> src/main.rs:16:62
   |
16 |     let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Square), Box::new(Rock)];
   |                                                              ^^^^^^^^^^^^^^ unsatisfied trait bound
   |
help: the trait `Shape` is not implemented for `Rock`
  --> src/main.rs:13:1
   |
13 | struct Rock;
   | ^^^^^^^^^^^
help: the trait `Shape` is implemented for `Square`
  --> src/main.rs:7:1
   |
 7 | impl Shape for Square {
   | ^^^^^^^^^^^^^^^^^^^^^
   = note: required for the cast from `Box<Rock>` to `Box<dyn Shape>`

In Rust a type joins the list by declaring impl Shape for Rock, and the compiler refuses the line that builds the list, so even the first 4 is not printed. The price is that you write the impl. For a closed set of types an enum is often the better answer (A trait or an enum?). See Traits and Trait objects.

7. Comprehensions are eager; Rust chains wait

Section titled “7. Comprehensions are eager; Rust chains wait”

Your habit: a list comprehension runs now, so [square(n) for n in nums] works for its side effects. A generator expression waits, and the run shows both orders.

Pythonh7_lazy.py
def square(n):
print("squaring", n)
return n * n
nums = [1, 2, 3]
as_list = [square(n) for n in nums]
print("list built")
as_gen = (square(n) for n in nums)
print("generator made")
print(list(as_gen))
$ python3 h7_lazy.py
squaring 1
squaring 2
squaring 3
list built
generator made
squaring 1
squaring 2
squaring 3
[1, 4, 9]
Rusth7_lazy.rs
fn square(n: i32) -> i32 {
println!("squaring {n}");
n * n
}
fn main() {
let nums = [1, 2, 3];
nums.iter().map(|&n| square(n));
println!("chain written, never consumed");
let lazy = nums.iter().map(|&n| square(n));
println!("iterator made");
let list: Vec<i32> = lazy.collect();
println!("{list:?}");
}
$ cargo run
warning: unused `Map` that must be used
 --> src/main.rs:8:5
  |
8 |     nums.iter().map(|&n| square(n));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
  |
8 |     let _ = nums.iter().map(|&n| square(n));
  |     +++++++

warning: `wordcount` (bin "wordcount") generated 1 warning

chain written, never consumed
iterator made
squaring 1
squaring 2
squaring 3
[1, 4, 9]

A Rust chain is shaped like your generator expression: map does nothing until something consumes it, here collect. A chain that nothing consumes is a compiler warning, and square did not print. So do not use map for side effects: write a for loop. See Iterators.

Your habit: a typo in an assignment makes a new attribute. @dataclass generates __init__ and __repr__, but by default the instance is still an ordinary object whose attributes can be added.

Pythonh8_class.py
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
p.xx = 10 # a typo, and also a brand new attribute
p.z = 3
print(p.__dict__)
$ python3 h8_class.py
{'x': 1, 'y': 2, 'xx': 10, 'z': 3}
Rusth8_struct.rs
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut p = Point { x: 1, y: 2 };
p.xx = 10;
println!("{} {}", p.x, p.y);
}
$ cargo run
error[E0609]: no field `xx` on type `Point`
 --> src/main.rs:8:7
  |
8 |     p.xx = 10;
  |       ^^ unknown field
  |
help: a field with a similar name exists
  |
8 -     p.xx = 10;
8 +     p.x = 10;

A Rust struct has the fields you declared, and the compiler suggests the one you probably meant. There is no inheritance either: a type that wants another type’s data holds it in a field (No inheritance). See Structs.

Your habit: share a counter between threads and rely on the GIL. In the standard CPython build one thread runs Python bytecode at a time, but that does not make “read, add, write back” one step. Here four threads each add one 2,000,000 times, with a function call between the read and the write.

Pythonh9_threads.py
import threading
count = 0
N = 2_000_000
def add_one(n):
return n + 1
def work():
global count
for _ in range(N):
count = add_one(count) # read, call a function, write back
threads = [threading.Thread(target=work) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print("expected:", 4 * N)
print("got: ", count)
$ python3 h9_threads.py
expected: 8000000
got:      5162450

This output changes from run to run. This is one real run.

Rusth9_threads.rs
use std::sync::{Arc, Mutex};
use std::thread;
const N: u64 = 2_000_000;
fn add_one(n: u64) -> u64 {
n + 1
}
fn main() {
let count = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..4)
.map(|_| {
let count = Arc::clone(&count);
thread::spawn(move || {
for _ in 0..N {
let mut guard = count.lock().unwrap();
*guard = add_one(*guard);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
println!("expected: {}", 4 * N);
println!("got: {}", *count.lock().unwrap());
}
$ cargo run
expected: 8000000
got:      8000000

The Python total differs from run to run and was short in the run above. A plain count += 1 loop printed the exact total in the lessons’ runs on this build, but that is how this build behaves, not a promise (Send and Sync shows both). I did not run a free-threaded Python build. Rust has no interpreter lock. The Mutex makes the total exact, and sharing an Rc with a thread is refused when the program is compiled:

$ cargo run
error[E0277]: `Rc<i32>` cannot be sent between threads safely
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(move || println!("{count}"));
  |                  ------------- -------^^^^^^^^^^^^^^^^^^^^
  |                  |             |
  |                  |             `Rc<i32>` cannot be sent between threads safely
  |                  |             within this `{closure@src/main.rs:6:32: 6:39}`
  |                  required by a bound introduced by this call
  |
  = help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<i32>`
note: required because it's used within this closure
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(move || println!("{count}"));
  |                                ^^^^^^^
  ... (notes pointing into the standard library trimmed)

Python threads are still useful for waiting on I/O; Threads measured which kinds of work overlap in this image. For the rest see Shared state.

10. async looks the same, and the runtime is your choice

Section titled “10. async looks the same, and the runtime is your choice”

Your habit: call hello() and expect it to start. In both languages it does not: the coroutine or future is a value that something has to drive. Python warns at run time, when the unused coroutine is collected. The asyncio docs say “simply calling a coroutine will not schedule it to be executed”.

Pythonh10_async.py
import asyncio
async def hello():
print("hello")
async def main():
hello()
print("after the call")
await hello()
asyncio.run(main())
$ python3 h10_async.py
/home/you/habits/h10_async.py:9: RuntimeWarning: coroutine 'hello' was never awaited
  hello()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
after the call
hello
Rusth10_async.rs
async fn hello() {
println!("hello");
}
fn main() {
hello();
println!("after the call");
}
$ cargo run
warning: unused implementer of `Future` that must be used
 --> src/main.rs:6:5
  |
6 |     hello();
  |     ^^^^^^^
  |
  = note: futures do nothing unless you `.await` or poll them
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default

warning: `wordcount` (bin "wordcount") generated 1 warning

after the call

Rust warns when the program is compiled, and hello did not print. The other difference: asyncio.run comes with Python, and the Rust standard library has the Future trait but no runtime (The standard library has no runtime), so you add one. See Async basics and Async with Tokio.

Both programs behind this table printed the same lines, and diff found no difference. The Python program prints through json.dumps so that its lines can be compared with Rust’s {:?}; plain print would show True and 'fig'. The left cell is a line of the Python program; the right cell is a line of the Rust program.

You write in Python In Rust
[n * n for n in nums] nums.iter().map(|n| n * n).collect::<Vec<_>>()
[n for n in nums if n % 2 == 0] nums.iter().filter(|n| *n % 2 == 0).collect::<Vec<_>>()
sorted(words, key=len) let mut w = words.clone(); w.sort_by_key(|s| s.len());
[f"{i}:{w}" for i, w in enumerate(words)] words.iter().enumerate().map(|(i, w)| format!("{i}:{w}")).collect::<Vec<_>>()
"b," in text text.contains("b,")
text.split(",") text.split(',').collect::<Vec<_>>()
", ".join(words) words.join(", ")
len(s) s.chars().count()
len(s.encode()) s.len()
xs[1:3] xs[1..3].to_vec()
d.get("c", 0) m.get("c").copied().unwrap_or(0)
x if x is not None else 5 x.unwrap_or(5)
try: return int(text) and except ValueError: return -1 text.parse::<i32>().unwrap_or(-1)
-7 // 2 (-7_i32).div_euclid(2)
2**100 2_u128.pow(100)
bool(items) !items.is_empty()
Path("note.txt").read_text() fs::read_to_string("note.txt").unwrap()
f"{42:>5}" format!("{:>5}", 42)
with Guard(): let _guard = Guard::new();, closed at the end of the scope
terminal
$ cargo run
map: [1, 4, 9, 16]
filter: [2, 4]
sorted by length: ["fig", "pear", "apple"]
enumerate: ["0:pear", "1:fig", "2:apple"]
contains: true
split: ["a", "b", "c"]
join: "pear, fig, apple"
chars: 5
bytes: 6
slice: [2, 3]
get default: 0
none default: 5
parse: -1
floor division: -4
big int: 1267650600228229401496703205376
truthy list: true
file: "hello\n"
format: "   42"
open
work
close
$ diff python.txt rust.txt
diff exit code: 0
21

Where the meaning differs and not only the spelling: Rust’s / truncates (-7 / 2 is -3), so the floor-division row uses div_euclid, which agrees with Python for a positive divisor (Division and remainder). len(s) counts code points and s.len() counts bytes, which is why the two len rows have different Rust spellings (Strings). unwrap() on the file row panics where Python raises FileNotFoundError; a function would return the error with ?. Three more rows look equal and are not:

terminal
$ python3 differs.py
int('1_000'): 1000
int(' 12 '): 12
xs[1:10]: [2, 3, 4]
sorted(words, key=len): ["éé", "abc"]
$ cargo run
"1_000".parse::<i32>(): Err(ParseIntError { kind: InvalidDigit })
" 12 ".parse::<i32>(): Err(ParseIntError { kind: InvalidDigit })
xs.get(1..10): None
sort_by_key(len): ["abc", "éé"]

thread 'main' (22) panicked at src/main.rs:13:36:
range end index 10 out of range for slice of length 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

int accepts underscores and spaces that parse rejects, xs[1:10] stops at the end where xs[1..10] panics (get returns None instead), and sorted(words, key=len) counts code points where s.len() counts bytes. For the rest of the Python spellings, each lesson has a table: Vectors, Maps, Strings, Text.

The Python side needs pip, and this image has Python but no pip, so the Python steps below install it and three tools into a virtual environment. They need network, and the packages’ own dependencies are not pinned. The Rust steps run offline because this project has no dependencies.

Job Python Rust
Project file pyproject.toml Cargo.toml
Add a dependency pip install pytest in a virtual environment cargo add serde (not run here; Using crates runs it)
Build none: python3 runs the file (python3 -m build makes a package; not run) cargo build
Run python3 wordcount.py cargo run
Test pytest -q cargo test
Format ruff format cargo fmt
Lint ruff check cargo clippy
Type check mypy part of the build
Docs python3 -m pydoc reads """docstrings""" cargo doc builds HTML from /// comments
Debugger python3 -m pdb (not run) rust-gdb, rust-lldb (not run)
Pythonpyproject.toml
[project]
name = "wordcount"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = []
[tool.pytest.ini_options]
testpaths = ["."]
RustCargo.toml
[package]
name = "wordcount"
version = "0.1.0"
edition = "2024"
[dependencies]

The Python half. The last step reads pyproject.toml with tomllib, which comes with Python:

terminal
$ python3 -m venv .venv   # fails in this image: no ensurepip
The virtual environment was not created successfully because ensurepip is not
available.  On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.

    apt install python3.13-venv

You may need to use sudo with that command.  After installing the python3-venv
package, recreate your virtual environment.

Failing command: /home/you/habits/.venv/bin/python3
$ python3 -m venv --without-pip .venv
$ curl -O https://bootstrap.pypa.io/get-pip.py && .venv/bin/python get-pip.py   # needs network; get-pip.py is not pinned
$ .venv/bin/pip install pytest==9.1.1 mypy==2.4.0 ruff==0.17.2   # needs network; their own dependencies are not pinned
$ .venv/bin/pytest -q
.                                                                        [100%]
1 passed in 0.00s
$ .venv/bin/ruff format --check wordcount.py test_wordcount.py; .venv/bin/ruff check wordcount.py test_wordcount.py
2 files already formatted
All checks passed!
$ python3 -m pydoc wordcount
Help on module wordcount:

NAME
    wordcount

FUNCTIONS
    word_count(text: str) -> collections.Counter[str]
        Count how often each word appears, ignoring case.

FILE
    /home/you/habits/wordcount.py
$ python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project'])"
{'name': 'wordcount', 'version': '0.1.0', 'requires-python': '>=3.13', 'dependencies': []}

This output changes from run to run. This is one real run.

The Rust half:

terminal
$ cargo build; echo "exit code $?"
exit code 0
$ cargo test
running 1 test
test tests::counts_ignoring_case ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo run
and: 1
cat: 1
hat: 1
the: 2
$ cargo fmt --check; echo "exit code $?"
exit code 0
$ cargo clippy; echo "exit code $?"
exit code 0
$ cargo doc --no-deps
 Documenting wordcount v0.1.0 (/home/you/habits)

   Generated target/doc/wordcount/index.html
$ ls $CARGO_HOME/bin
cargo	      cargo-miri     rust-analyzer  rust-lldb  rustfmt
cargo-clippy  clippy-driver  rust-gdb	    rustc      rustup
cargo-fmt     rls	     rust-gdbgui    rustdoc

The lessons in the order that suits a Python reader:

  1. Rust at a glance: one page of syntax, with a Python column.
  2. What Rust lacks: most of your habits fail loudly, such as truthiness and default arguments; // does not, it starts a comment.
  3. Types: fixed-size integers instead of one int.
  4. Stack and heap and Copy or reference?: why b = a means something else here.
  5. Ownership, Borrowing, Mutable borrows: habit 2 in full, and the heart of the language.
  6. Option and Errors: None and exceptions, replaced.
  7. Structs, Methods, Enums: classes and dataclasses, without inheritance.
  8. Closures and Iterators: lambdas and comprehensions.
  9. Traits and Trait objects: Protocol and duck typing.
  10. Modules: a file joins a crate when its parent says mod, not when someone imports it.
  11. Working with the borrow checker: once the errors start arriving.
  12. Threads, Shared state, Send and Sync: the GIL’s replacement.
  13. Async basics, then Async with Tokio.
  14. Using Rust from Python: a Rust extension module that Python imports.
  • Running half-written code. h6_duck.py printed 4 before it failed. The Rust version printed nothing, because a program with a type error is not built. todo!() stands in for a body you have not written yet, so the rest can still compile.
  • A REPL. python3 opens one. None of the tools in the listing above (cargo, rustc, rustfmt, rust-analyzer and others) reads a line of Rust and runs it, and the Rust Playground runs short programs in a browser.
  • Open objects. Habit 8 was a mistake, and it was also a feature: adding attributes as you go has no counterpart in a struct. A HashMap is the nearest container for data whose keys you do not know in advance.
  • Unbounded integers and decimals. 2**200 has no built-in Rust type (Big numbers are not built in).
  • Your data libraries. pandas and NumPy are packages you install, and the Rust counterparts are crates. The DataFusion guide runs one job with a Rust query engine and with several Python data libraries.

Each task starts from one of the programs above. The sessions were run in the image.

  1. In h5_none.rs, print users.get("bob").map(|name| name.to_uppercase()) with {:?} instead of calling to_uppercase on the Option. It prints None and compiles. Change "bob" to "ada" and it prints the name inside Some(...).
  2. In h6_duck.rs, add impl Shape for Rock with an area that returns 0. The program compiles and prints 4 and 0.
  3. In h2_fixed.rs, change add_bonus(&mut a) to add_bonus(&a). The error says the types differ in mutability: the call must agree with the signature.
terminal
$ cargo run    # after changing the println! line to users.get("bob").map(|name| name.to_uppercase()) printed with {:?}
None
$ cargo run    # after changing "bob" to "ada"
Some("ADA LOVELACE")
terminal
$ cargo run    # after adding impl Shape for Rock { fn area(&self) -> u32 { 0 } }
4
0
$ cargo run # after changing add_bonus(&mut a) to add_bonus(&a)
error[E0308]: mismatched types
  --> src/main.rs:10:15
   |
10 |     add_bonus(&a);
   |     --------- ^^ types differ in mutability
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected mutable reference `&mut Vec<i32>`
                      found reference `&Vec<{integer}>`
note: function defined here
  --> src/main.rs:1:4
   |
 1 | fn add_bonus(scores: &mut Vec<i32>) {
   |    ^^^^^^^^^ ---------------------

For more information about this error, try `rustc --explain E0308`.
error: could not compile `wordcount` (bin "wordcount") due to 1 previous error