Work in progress

A Little Rust, Side by Side

A quick-reference Python-to-Rust cheat sheet for the project and book

Use this page as a cheat sheet

This is a quick information-retrieval page, not a chapter to memorize. When Rust syntax hides the idea, scan the headings or search for the operation, read the two columns, and return to the code.

The goal is simple: you can read all the Rust code used in the project and book. You do not need to reproduce every spelling from memory, and it is expected that you will return here when you need one.

You do not need to be good at Python. The left side is only a recognition aid: find an operation you have seen, then read across to its Rust spelling. If the Python is also new, the notice below each pair says what matters.

These are small fragments, not one long program. Later fragments reuse City, Road, and roads after they have appeared. Each fragment isolates one common Rust spelling.

01

Put statements in a program

Python
city = "Logan"
Rust
fn main() {
    let city = "Logan";
}
02

Change a binding

Python
city = "Logan"
city = "Provo"
Rust
let mut city = "Logan";
city = "Provo";

Read a local value, a constant, and a block result

03

Return a value from a small computation

Python
LIMIT = 40

def answer():
    compiled = 6
    return compiled + 1

value = answer()
Rust
const LIMIT: i64 = 40;

let value: i64 = {
    let compiled = 6;
    compiled + 1
};
04

Keep two values in order

Python
road = ("Logan", "Salt Lake City")
reversed_road = ("Salt Lake City", "Logan")

road == reversed_road  # False
Rust
let road = ("Logan", "Salt Lake City");
let reversed_road = ("Salt Lake City", "Logan");

road == reversed_road  // false
05

Give the tuple shape a name

Python
road = ("Logan", "Salt Lake City")
Rust
type City = &'static str;
type Road = (City, City);

let road: Road = ("Logan", "Salt Lake City");
06

Keep an ordered, growable sequence

Python
roads = []
roads.append(("Logan", "Salt Lake City"))
roads.append(("Logan", "Provo"))
Rust
let mut roads: Vec<Road> = Vec::new();
roads.push(("Logan", "Salt Lake City"));
roads.push(("Logan", "Provo"));
07

Keep a set of unique roads

Python
roads = set()
roads.add(("Logan", "Salt Lake City"))
roads.add(("Logan", "Salt Lake City"))

count = len(roads)  # 1
Rust
use std::collections::HashSet;

let mut roads: HashSet<Road> = HashSet::new();
roads.insert(("Logan", "Salt Lake City"));
roads.insert(("Logan", "Salt Lake City"));

let count = roads.len();  // 1

Read a generic type

Vec<T> describes a family of sequence types. The T is a type parameter: a placeholder for the element type. Writing Vec<Road> chooses Road for that parameter. HashSet<T> and Option<T> use the same idea.

08

Choose the element type

Python
city_list: list[str] = [
    "Logan",
    "Provo",
]
road_list: list[tuple[str, str]] = [
    ("Logan", "Provo"),
]
Rust
let city_list: Vec<City> = vec![
    "Logan",
    "Provo",
];
let road_list: Vec<Road> = vec![
    ("Logan", "Provo"),
];

Read a Rust name or call

SpellingRead it as
fn f(x: T) -> UDefine f: it receives an x of type T and returns a U.
f(x)Call the function f.
x.f()Call a method on x.
T::f()Call a function associated with type T.
module::TFollow a namespace path to T.
f::<T>(x)Call generic f while explicitly choosing type T.
use module::T;Bring T into the current scope so its shorter name can be used.

The same :: path separator appears in names such as std::collections::HashSet and syn::Expr. The ::<T> spelling is sometimes called the *turbofish*; the important fact is simply that it tells Rust which type this call should produce or use.

Test membership in a collection

Python spells membership with in. Rust collections provide methods for the same question: HashSet<T> uses contains, while iterator predicates such as any answer more general “does a matching item exist?” questions. A named membership function can wrap contains when the question belongs to the vocabulary of the program.

09

Test one candidate

Python
candidate = ("Logan", "Provo")
present = candidate in roads
Rust
let candidate: Road = ("Logan", "Provo");
let present = roads.contains(&candidate);
10

Let a function take the set

Python
def is_road(roads, road):
    return road in roads

first = is_road(roads, ("Logan", "Provo"))
second = is_road(roads, ("Ogden", "Logan"))
Rust
fn is_road(roads: HashSet<Road>, road: Road) -> bool {
    roads.contains(&road)
}

let first = is_road(roads, ("Logan", "Provo"));
let second = is_road(roads, ("Ogden", "Logan"));
//                   ^^^^^ value used after move
11

Borrow the set instead

Python
def is_road(roads, road):
    return road in roads

first = is_road(roads, ("Logan", "Provo"))
second = is_road(roads, ("Ogden", "Logan"))
Rust
fn is_road(
    roads: &HashSet<Road>,
    road: Road,
) -> bool {
    roads.contains(&road)
}

let first = is_road(&roads, ("Logan", "Provo"));
let second = is_road(&roads, ("Ogden", "Logan"));
12

Separate reading from changing

Python
before = len(roads)
answer = is_road(roads, ("Logan", "Provo"))
after = len(roads)

(before, answer, after)  # (1, False, 1)
Rust
let before = roads.len();
let answer = is_road(&roads, ("Logan", "Provo"));
let after = roads.len();

(before, answer, after)  // (1, false, 1)
13

Use a plain loop

Python
count = 0
for _road in roads:
    count += 1
Rust
let mut count = 0;
for _road in &roads {
    count += 1;
}

Pass a small operation with a closure

A function can be named once and called later. A closure is an unnamed function-like value that can be stored, passed to another operation, and use names from its surroundings.

14

Write a lambda or closure

Python
origin = "Logan"
destination = lambda road: road[1]
starts_here = lambda road: road[0] == origin

city = destination(("Logan", "Provo"))
answer = starts_here(("Logan", "Provo"))
Rust
let origin = "Logan";
let destination = |road: &Road| road.1;
let starts_here = |road: &Road| road.0 == origin;

let city = destination(&("Logan", "Provo"));
let answer = starts_here(&("Logan", "Provo"));

Build an iterator pipeline

Collections hold values. An iterator describes how to visit them. In Rust, iter() borrows the collection, iterator adaptors such as map and filter describe a pipeline, and a consumer such as collect, count, or any drives that pipeline.

15

Borrow items with iter

Python
road_iter = iter(roads)
first = next(road_iter, None)
still_here = len(roads)
Rust
let mut road_iter = roads.iter();
let first: Option<&Road> = road_iter.next();
let still_here = roads.len();
16

Take ownership with into_iter

Python
road_iter = iter(roads)
first = next(road_iter, None)

# Python still keeps roads here.
Rust
let mut road_iter = roads.into_iter();
let first: Option<Road> = road_iter.next();

// roads.len();  // error: roads was moved
17

Transform every item with map

Python
destinations = list(map(
    lambda road: road[1],
    roads,
))
Rust
let destinations: Vec<City> = roads
    .iter()
    .map(|road| road.1)
    .collect();
18

Keep matching items with filter

Python
from_logan = list(filter(
    lambda road: road[0] == "Logan",
    roads,
))
Rust
let from_logan: Vec<&Road> = roads
    .iter()
    .filter(|road| road.0 == "Logan")
    .collect();
19

Ask whether an item exists

Python
has_provo = any(
    road[1] == "Provo"
    for road in roads
)
first_to_provo = next(
    (road for road in roads
     if road[1] == "Provo"),
    None,
)
Rust
let has_provo = roads
    .iter()
    .any(|road| road.1 == "Provo");
let first_to_provo: Option<&Road> = roads
    .iter()
    .find(|road| road.1 == "Provo");
20

Reduce many items to one value

Python
total = sum(
    len(road[1])
    for road in roads
)
Rust
let total: usize = roads
    .iter()
    .map(|road| road.1.len())
    .sum();

let same_total = roads.iter().fold(
    0,
    |total, road| total + road.1.len(),
);

Common iterator vocabulary

PurposePython spellingRust iterator spelling
Transform each itemmap(f, items).map(f)
Keep matching itemsfilter(p, items).filter(p)
Run an action for each itemfor item in items: f(item).for_each(f)
Transform and discard missing resultscomprehension with a condition.filter_map(f)
Produce several items from each itemnested comprehension.flat_map(f) or .flatten()
Attach positionsenumerate(items).enumerate()
Pair two sequenceszip(left, right).zip(right)
Visit one sequence after anotheritertools.chain(left, right).chain(right)
Ignore or limit a prefixitertools.islice(...).skip(n) or .take(n)
Find one matching itemnext((x for x in items if p(x)), None).find(p)
Test some or all itemsany(...) or all(...).any(p) or .all(p)
Reduce itemssum(...) or functools.reduce(...).sum() or .fold(initial, f)
Materialize resultslist(...) or set(...).collect::<Vec<_>>() or .collect::<HashSet<_>>()

Use iter() when the pipeline should read borrowed items and into_iter() when it should own the items. Most adaptors are lazy; consumers such as next, collect, find, any, all, count, sum, and fold request results.

Give a value named fields with a struct

A Rust struct defines one fixed shape. Every value of that type has the same named fields, although the field values may differ.

21

Define one record shape

Python
from dataclasses import dataclass

@dataclass(frozen=True)
class Atom:
    relation: str
    arguments: tuple[str, ...]

atom = Atom(
    relation="road",
    arguments=("x", "y"),
)
Rust
struct Atom {
    relation: &'static str,
    arguments: Vec<&'static str>,
}

let atom = Atom {
    relation: "road",
    arguments: vec!["x", "y"],
};

Ask for and define a default value

Rust's Default trait means “this type has a conventional starting value.” It does not invent a value for every type: a type must implement Default, and the meaning should be unsurprising for that type.

22

Ask for a type's default

Python
roads: set[tuple[str, str]] = set()
limit: int = 0
maybe_road: tuple[str, str] | None = None
Rust
let roads: HashSet<Road> = Default::default();
let limit: usize = Default::default();
let maybe_road: Option<Road> = Default::default();
23

Use a default only when a value is missing

Python
index: dict[str, list[tuple[str, str]]] = {}
index.setdefault("Logan", []).append(
    ("Logan", "Provo")
)

maybe_limit: int | None = None
limit = 0 if maybe_limit is None else maybe_limit
Rust
use std::collections::HashMap;

let mut index: HashMap<City, Vec<Road>> = HashMap::new();
index.entry("Logan").or_default().push(
    ("Logan", "Provo")
);

let maybe_limit: Option<usize> = None;
let limit = maybe_limit.unwrap_or_default();
24

Derive field-by-field defaults

Python
from dataclasses import dataclass

@dataclass(frozen=True)
class QueryOptions:
    distinct: bool = False
    limit: int = 0

ordinary = QueryOptions()
limited = QueryOptions(limit=10)
Rust
#[derive(Default)]
struct QueryOptions {
    distinct: bool,
    limit: usize,
}

let ordinary = QueryOptions::default();
let limited = QueryOptions {
    limit: 10,
    ..Default::default()
};

If field-by-field defaults are not the intended meaning, write an explicit impl Default instead of deriving one. If the domain has no honest conventional value, do not implement Default; require the caller to construct the value explicitly.

Recognize macro-related spellings

SpellingRead it as
name!(...)Invoke a function-like macro. {...} and [...] may also delimit its input.
#[derive(X)]Ask a derive macro to generate an implementation of trait X for the following type.
#[proc_macro]Register the following public function as a function-like procedural-macro entry point.

#[derive(Default)] above is an ordinary example of the second form. The name!(...) spelling alone does not reveal whether the macro is built into Rust, defined declaratively, or implemented as a procedural macro. It identifies the invocation shape, not the implementation mechanism. #[proc_macro] registers an implementation; it does not invoke that implementation.

Represent alternatives with an enum

A data-carrying Rust enum is a tagged, disjoint union—a sum type—not a subtype hierarchy. For this syntax tree, the type has the shape

Expr = Name(String) + Call(String × List(Expr)).

The + means “one of these tagged alternatives”; the × means that the fields occur together. A constructor builds one alternative as an Expr. A pattern match reads the tag and exposes that alternative's payload.

25

Define the same AST union

Python
from dataclasses import dataclass

@dataclass(frozen=True)
class Name:
    text: str

@dataclass(frozen=True)
class Call:
    function: str
    arguments: list["Expr"]

Expr = Name | Call  # a union type alias

program: list[Expr] = [
    Name("x"),
    Call("f", [Name("x")]),
]
Rust
enum Expr {
    Name(String),
    Call {
        function: String,
        arguments: Vec<Expr>,
    },
}

let program: Vec<Expr> = vec![
    Expr::Name(String::from("x")),
    Expr::Call {
        function: String::from("f"),
        arguments: vec![
            Expr::Name(String::from("x")),
        ],
    },
];

A pattern can recognize the current form and bind names to parts of its payload in one step.

26

Match an AST form and bind its fields

Python
from typing import assert_never

def child_count(expr: Expr) -> int:
    match expr:
        case Name(text=_):
            return 0
        case Call(arguments=children):
            return len(children)
        case _ as impossible:
            assert_never(impossible)
Rust
fn child_count(expr: &Expr) -> usize {
    match expr {
        Expr::Name(_) => 0,
        Expr::Call {
            arguments: children,
            ..
        } => {
            children.len()
        }
    }
}

Why subtyping is the wrong model here

Declaring Name and Call as subclasses of an Expr base class describes an open family, not the closed sum above. Upcasting a node to Expr hides its alternative-specific fields. Recovering them requires runtime narrowing, a partial downcast, virtual methods, or a visitor. Adding another subclass also does not force every existing case analysis to be rechecked.

Mutable containers expose another mismatch: a list[Name] cannot safely become a list[Expr], because code holding the latter could insert a Call. Strict type checkers therefore reject that conversion. Declare the heterogeneous container at the union type from the beginning: Python uses list[Expr]; Rust uses Vec<Expr>.

For a known AST, do not introduce a Rust trait such as ExprNode and Vec<Box<dyn ExprNode>> merely to hold several node forms. Trait objects provide open polymorphism and dynamic dispatch; an enum directly represents the required closed sum.

Return to A Little Database, A Bit Rustic and use this cheat sheet whenever Rust spelling hides the database idea. Finding the spelling quickly and continuing to read is the skill this page is meant to build.