My Skills are Starting To Get... Rusty

It’s been awhile since I’ve gone back to basics and become a beginner again on a programming language. Learning a new one is tough after you’ve been ingrained in one community for so long.
Inspiration
I’ve been on the fence about learning a new language for awhile now. I’ve used Python for most of my professional career. I’ve used languages like PowerShell, Lua, and Javascript when necessary, but have always returned to what I’m comfortable with.
I’ve never used a compiled or systems language outside of college, and felt it time to question my assumptions. Like learning a spoken language, learning a programming language that has drastically different paradigms makes you think differently (hopefully better) when you’re presented with a challenge.



I’ve been going back and forth between learning Go and Rust, while at the same time thinking I’ve never really hit a wall with Python that would force a switch. Rust has been at the top of the leaderboards for speed and safety for awhile now, and is at the top of the list for the most desired language according to the Stack Overflow survey.
What really triggered me to dive into Rust is listening to the Ship It podcast where Tim McNamara talked about how he’s been evangelizing Rust at AWS. Rust isn’t just going to be saving cycles, it’ll be improving sustainability. It’ll be saving money. Looking at the Lambda cold start times and extrapolating over all the resources that could benefit from it, there’s going to be a huge reduction in resources needed.
Immersion
Reading Rust code feels like I’m deciphering hieroglyphics. I knew from the start I would need to immerse myself in the code in order to learn it.
These are the resources I used within this first week of learning Rust.
- Ship It podcast - Rust efficiencies at AWS scale
- Rust in Action Book by Tim McNamara. So far it’s an easy read and extremely helpful to decipher the meaning of Rust semantics.
- The Rust Programming Language Book (html)
- No Boilerplate - Youtube video - Rust for the Impatient which covers the next article in video form
- Learn x in y minutes - Rust - Simple reference to come back to every now and then
- Rustacean Station Podcast
- CS 128 Honors @ Illinois Fall 2022 - Rust
- rustlings - A kata-like approach to learning Rust with small exercises to get you more exposed to the language
Thanks to these resources, I’m getting a lot better at reading code written in Rust. I’m still not 100% confident in writing it.
Be Deliberate
Learning can’t be passive. Use some or all of these approaches to start getting some muscle-memory in the language.
- Add to spaced repetition system (Anki)
- Use coding “katas”, like rustlings
- Model your existing workflows in the new language (see below)
- Blog about it so you have a reason to learn about key aspects
For me, I really want to see how my workflows might change by switching to Rust. Let’s take a look at some of them.
Comparing workflows
For those coming from Python, here are some of the differences. I prefer to have a bunch of different standalone examples in order to gauge if it’s something I’m comfortable with day to day.
Starting a new project
Python
With Python, you get in the habit of first making/sourcing a virtual environment.
# python
mkdir py-api && cd py-api
# virtualenv
python3.11 -m venv venv
source venv/bin/activate
pip install --upgrade pip
# dependencies
echo "Flask" >> requirements.txt # api framework
pip install -r requirements.txt
Rust
For rust, we use cargo to set up the directory and dependencies for us. I admittedly chose a more complex start than is usually necessary, so you’ll see I’m overriding the standard toolchain for Rust with the nightly build.
# rust
cargo new rust-api && cd rust-api
rustup update && cargo update
# dependencies
rustup override set nightly && cargo add rocket # api framework, requires nightly build
cargo build # or: cargo run, to both build and run
One thing I noticed is that if you don’t have a nightly build of rust installed (rustup toolchain list
), adding the rocket
dependency will download it for you so long as you’ve overridden the build for your current project. When you’re in a project that needs a different build, you’ll see that build as overriding the default.
Not overridden
rustup toolchain list
# stable-aarch64-apple-darwin (default)
# nightly-aarch64-apple-darwin
Overridden
rustup toolchain list
# stable-aarch64-apple-darwin (default)
# nightly-aarch64-apple-darwin (override)
If you want to explicitly download that build, you can with: rustup toolchain install nightly
.
Finding packages
Python
Use pypi, the Python Package Index, which pip uses by default.
Rust
Go to crates.io, which Cargo uses by default.
APIs
Let’s model a simple API in Python and Rust. We’ll have two endpoints that can be hit:
/hello/<person>/<age>
/world/<planet>
For Python, we’ll use Flask, a common web framework for building APIs. For Rust, I chose Rocket without doing too much research. It seems easy to use, but immediately I ran into needing a non-standard rust toolchain.
Python
Here’s a simple Flask app listening on port 5000 for a couple different routes.
main.py
# Run: source venv/bin/activate && python main.py
from flask import Flask
app = Flask(__name__)
@app.route("/hello/<name>/<age>")
def hello(name: str, age: int):
return f"<p>Hello, {age} year old named {name}!</p>"
@app.route("/world/<planet>")
def world(planet: str):
return f"<p>Hello, from planet {planet}!</p>"
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0")
requirements.txt
Flask
URL examples:
- http://127.0.0.1:5000/hello/erik/35
- http://127.0.0.1:5000/world/earth
Rust
Here’s a Rocket API to do the same thing. By default, it’ll be listening on port 8000. As a beginner, my eyes just try to ignore all the attributes for now (e.g. #
). I know they’re necessary, but I’ll come back to understanding them later. See attributes or procedural-macros for more info.
src/main.rs
// Run: cargo run
// inner attribute - apply to the item that the attribute is declared within
#![feature(proc_macro_hygiene, decl_macro)]
// outer attribute - apply to the thing that follows the attribute
#[macro_use] extern crate rocket;
// HTTP methods use outer attributes
// because /hello maps to this function, full path=/hello/name/age
#[get("/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
// because /world maps to this function, full path=/world/planet
#[get("/<planet>")]
fn world(planet: String) -> String {
format!("Hello from planet {}!", planet)
}
fn main() {
rocket::ignite()
.mount("/hello", routes![hello])
.mount("/world", routes![world])
.launch();
}
Cargo.toml
[package]
name = "rust-api"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = "0.4.11"
URL examples:
- http://127.0.0.1:8000/hello/erik/35
- http://127.0.0.1:8000/world/earth
Building CLI tooling
Let’s make a simple CLI app to update our local tooling managed by brew.
We want to run the following shell script, but have it accept new arguments to add runtime features.
#!/bin/zsh
# Place in /scripts/brew-upgrade-packages-and-cleanup.sh
# crontab: 0 8,20 * * * /scripts/brew-upgrade-packages-and-cleanup.sh > /scripts/upgrade-brew.log 2>&1
/opt/homebrew/bin/brew update
/opt/homebrew/bin/brew upgrade
/opt/homebrew/bin/brew upgrade pyenv
/opt/homebrew/bin/brew cleanup -s
/opt/homebrew/bin/terminal-notifier -message "Brew upgrade/cleanup complete"
An upgraded version that accepts arguments like update
would look like the following.
zsh
Example code
#!/bin/zsh
run_brew_commands() {
local commands
commands=(
"/opt/homebrew/bin/brew update"
"/opt/homebrew/bin/brew upgrade"
"/opt/homebrew/bin/brew upgrade pyenv"
"/opt/homebrew/bin/brew cleanup -s"
"terminal-notifier -message 'Brew upgrade/cleanup complete'"
)
for command in "${commands[@]}"; do
eval $command
done
}
main() {
if [[ "$1" == "update" ]]; then
run_brew_commands
echo "Brew update and upgrade completed."
else
echo "Usage: $0 update"
fi
}
main "$@"
Python
Example code
# main.py
# run: python main.py
# run update: python main.py update
import argparse
import subprocess
def run_brew_commands():
commands = [
"/opt/homebrew/bin/brew update",
"/opt/homebrew/bin/brew upgrade",
"/opt/homebrew/bin/brew upgrade pyenv",
"/opt/homebrew/bin/brew cleanup -s",
"terminal-notifier -message 'Brew upgrade/cleanup complete'",
]
for command in commands:
subprocess.run(command, shell=True)
def main():
parser = argparse.ArgumentParser(description="Brew Update and Upgrade CLI")
parser.add_argument("action", nargs="?", help="Action to perform (update)")
args = parser.parse_args()
if args.action == "update":
print("Starting application update")
run_brew_commands()
print("Update complete")
else:
parser.print_help()
if __name__ == "__main__":
main()
Rust
Example code
// src/main.rs
// dependencies: cargo add clap@2
// run: cargo run
// run update: cargo run -- update
extern crate clap;
use clap::{App, Arg};
use std::process::Command;
fn run_brew_commands() {
let commands = vec![
"/opt/homebrew/bin/brew update",
"/opt/homebrew/bin/brew upgrade",
"/opt/homebrew/bin/brew upgrade pyenv",
"/opt/homebrew/bin/brew cleanup -s",
"/opt/homebrew/bin/terminal-notifier -message 'Brew upgrade/cleanup complete'",
];
for command in commands {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(command);
let output = cmd.output().expect("Failed to execute command");
if !output.status.success() {
println!("Error running command: {}", command);
println!("{}", String::from_utf8_lossy(&output.stderr));
std::process::exit(1);
}
}
}
fn main() {
let matches = App::new("Brew Updater")
.version("1.0")
.about("Update and upgrade Homebrew packages")
.arg(
Arg::with_name("action")
.help("The action to perform")
.required(true)
.possible_values(&["update"]),
)
.get_matches();
let action = matches.value_of("action").unwrap();
if action != "update" {
println!("Usage: brew_updater update");
std::process::exit(1);
}
run_brew_commands();
println!("Brew update and upgrade completed.");
}
Testing
Python
After setting up the dependencies and files, run by calling: pytest
.
# python
mkdir py-tester && cd py-tester
# virtualenv
python3.11 -m venv venv
source venv/bin/activate
pip install pytest
pytest
main.py
def sum(a: int, b: int):
"""Calculate the sum of two integers."""
return a + b
def is_positive(num: int):
"""Check if a number is positive."""
return num > 0
def is_odd(num: int):
"""Check if a number is odd."""
return num % 2 != 0
tests/test_main.py
from main import sum, is_positive, is_odd
def test_sum():
assert sum(2, 3) == 5
assert sum(-1, 1) == 0
def test_is_positive():
assert is_positive(5) is True
assert is_positive(-1) is False
assert is_positive(0) is False
def test_is_odd():
assert is_odd(1) is True
assert is_odd(-3) is True
assert is_odd(2) is False
assert is_odd(0) is False
Rust
No dependencies are necessary outside of the standard library. Here’s some information on test organization in Rust.
Run with: cargo test
.
src/main.rs
// main.rs
// Function to calculate the sum of two integers
fn sum(a: i32, b: i32) -> i32 {
a + b
}
// Function to check if a number is positive
fn is_positive(num: i32) -> bool {
num > 0
}
// Function to check if a number is odd
fn is_odd(num: i32) -> bool {
num % 2 != 0
}
fn main() {
let num1 = 5;
let num2 = 7;
println!("Sum of {} and {} is: {}", num1, num2, sum(num1, num2));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum(2, 3), 5);
assert_eq!(sum(-1, 1), 0);
}
#[test]
fn test_is_positive() {
assert!(is_positive(5));
assert!(!is_positive(-1));
assert!(!is_positive(0));
}
#[test]
fn test_is_odd() {
assert!(is_odd(1));
assert!(is_odd(-3));
assert!(!is_odd(2));
assert!(!is_odd(0));
}
}
Build a binary
Here we use the CLI example from above.
Python
The packaging story with Python is difficult and filled with pitfalls. This post doesn’t attempt to go into any detail as it’s been covered at length by others.
Rust
If we used the CLI tooling example, we could create an executable binary with the following:
# rust-cli-app
cargo build --release
./target/release/rust-cli-app # run it
./target/release/rust-cli-app update # run it with arguments
# rust-api
cargo build --release
./target/release/rust-api # run it
# curl localhost:8000/hello/erik/35
# curl localhost:8000/world/earth
Package your app with Docker
Here we use the API example from above and package it with Docker. We use a build script called build.sh to make it simple to build and reuse.
Change the image name, ports exposed, and test steps based on which app you’re testing.
Python
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "main.py"]
build.sh
#!/bin/zsh
IMAGE="python_api_app"
echo "Cleaning up any existing containers..."
docker rm -f $IMAGE
echo "Building the image..."
docker build --platform linux/amd64 -f Dockerfile -t $IMAGE .
echo "Running the container..."
docker run -dit --platform linux/amd64 -p 8100:5000 --name $IMAGE $IMAGE
echo "Testing..."
sleep 2
curl localhost:8100/hello/erik/35
echo ""
curl localhost:8100/world/earth
echo "\n"
Rust
Dockerfile
# FROM rust:latest as builder, if you don't need nightly
FROM rustlang/rust:nightly as builder
WORKDIR /app
COPY . .
RUN cargo build --release
# or FROM rockylinux:9.2-minimal
# or FROM amazonlinux:2
FROM debian:bullseye-slim
WORKDIR /app
COPY --from=builder /app/target/release/rust-api /app/rust-api
RUN chmod +x /app/rust-api
CMD ["/app/rust-api"]
build.sh
#!/bin/zsh
IMAGE="rust_api_app"
echo "Cleaning up any existing containers..."
docker rm -f $IMAGE
echo "Building the image..."
docker build --platform linux/amd64 -f Dockerfile -t $IMAGE .
echo "Running the container..."
docker run -dit --platform linux/amd64 -p 8000:8000 --name $IMAGE $IMAGE
echo "Testing..."
sleep 2
curl localhost:8000/hello/erik/35
echo ""
curl localhost:8000/world/earth
echo "\n"
Troubleshooting Rust
You’re trying to use a nightly build but it keeps using the stable build
If you installed Rust with homebrew, rustc might be at the wrong path (which rustc
should show it in ~/.cargo/bin/rustc). It’s recommended to brew uninstall rust && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
to install with the recommended approach from rustup.rs.