Lewati ke konten

Rust untuk Developer Python: Panduan Praktis

Kalau kamu sudah bisa Python, sebenarnya kamu sudah mengenal banyak ide Rust. Rust punya perulangan for atas iterator, tuple, closure (mirip lambda di Python, tapi lebih berguna), match yang bekerja seperti match di Python, dan rantai iterator yang mengerjakan tugas list comprehension. Rust bahkan meminjam sedikit selera Python: satu cara yang jelas untuk memformat kode, tool package yang ramah, dan pesan error yang berusaha keras membantumu.

Yang baru sebagian besar menyangkut compiler. Python mengecek tipe saat programmu berjalan; Rust mengecek semuanya sebelum program berjalan. Python membersihkan memori dengan garbage collector; Rust memakai ownership (kepemilikan) dan borrowing (peminjaman), yang dicek saat compile. Python punya None dan exception; Rust punya Option dan Result. Tiga hal inilah yang akan menyita sebagian besar waktu belajarmu.

Lalu, kenapa developer Python belajar Rust? Alasan jujurnya kira-kira begini:

  • Kecepatan. Rust di-compile menjadi kode mesin native. Pekerjaan berat di CPU yang lambat di Python murni (parsing, hitung-hitungan angka, perulangan besar) biasanya jauh lebih cepat di Rust, tanpa trik tuning apa pun.
  • Thread sungguhan. Di CPython standar, GIL hanya mengizinkan satu thread menjalankan kode Python pada satu waktu. Thread Rust berjalan di semua core CPU-mu sekaligus, dan compiler menghentikan data race sebelum programmu berjalan.
  • Bug ketahuan lebih awal. Salah ketik nama atribut, None yang terlupa, atau fungsi yang dipanggil dengan tipe yang salah: di Python kamu baru menemukannya saat baris itu dijalankan. Di Rust, program tidak mau di-compile sampai semuanya diperbaiki.
  • Kamu tidak harus memilih. Dengan PyO3 dan maturin, kamu bisa menulis bagian yang lambat dari proyek Python dengan Rust, lalu meng-import-nya dari Python seperti modul biasa. Tool Python populer seperti Polars, Ruff, dan uv ditulis dengan Rust.

Kalau kamu mencari perbandingan yang lebih umum antara Rust dengan Python, Go, dan C++, lihat Rust vs C++, Go, dan Python.

Pakai tabel ini seperti kamus percakapan. Kolom terakhir berisi link ke pelajaran di situs ini yang menjelaskan sisi Rust-nya.

Python Rust Pelajari lebih lanjut
Tipe dinamis, dicek saat runtime Tipe statis, dicek saat compile. Rust menebak (infer) sebagian besar tipe, jadi kamu menulis tipe lebih sedikit daripada yang kamu kira Tipe data
Type hint (def f(x: int) -> str) Tipe di signature fungsi wajib ditulis, dan compiler menegakkannya Fungsi
x = 5 let x = 5; (tidak bisa diubah). let mut x = 5; kalau kamu ingin mengubahnya Variabel
int (bisa membesar sebesar apa pun) Integer berukuran tetap: i32, i64, u64, usize, i128… Bisa overflow Tipe data, Integer overflow
float f64 (atau f32) Tipe data
str String (dimiliki, bisa bertambah panjang) dan &str (tampilan teks yang dipinjam) String
f-string: f"Hi {name}" format!("Hi {name}") dan println!("Hi {name}") Output
print(repr(x)) / __repr__ println!("{x:?}") dengan #[derive(Debug)] Output, Trait yang sering dipakai
__str__ Implementasikan Display (lalu .to_string() juga langsung bisa dipakai) Trait yang sering dipakai
__eq__, __hash__, __lt__ Derive PartialEq, Eq, Hash, PartialOrd, Ord Trait yang sering dipakai
None, Optional[T] Option<T>: bisa Some(value) atau None Option
x or default x.unwrap_or(default) Option
raise, try/except Result<T, E> + operator ? Result, Error kustom
Exception untuk bug sungguhan (assert, IndexError) panic! (menghentikan thread) Panic
class dengan __init__ dan method struct untuk datanya + blok impl untuk method-nya; new hanyalah fungsi biasa Struct
@dataclass struct dengan #[derive(Debug, Clone, PartialEq)] Struct
self &self (membaca), &mut self (mengubah), atau self (mengambil ownership) Struct
Inheritance Tidak ada inheritance. Pakai komposisi (struct di dalam struct) + trait dengan method default Trait, Design pattern
Duck typing, Protocol, ABC trait Trait
List berisi objek dari class yang berbeda-beda Vec<Box<dyn Trait>> (sebuah trait object) Trait object
TypeVar / fungsi generik Generics dengan trait bound: fn largest<T: Ord>(items: &[T]) Generik
Class Enum enum yang setiap varian-nya bisa membawa datanya sendiri Enum
match / case (3.10+) match (compiler mengecek bahwa semua kasus sudah kamu tangani) Match, Pattern matching
if my_list: (truthiness) Tulis secara eksplisit: if !my_list.is_empty(). Hanya bool yang boleh masuk ke if If…Else
for i in range(10) for i in 0..10 Perulangan for
enumerate(xs), zip(a, b) xs.iter().enumerate(), a.iter().zip(b.iter()) Iterator
lambda x: x + 1 Closure: |x| x + 1 (dan boleh lebih dari satu baris) Closure
List comprehension, map, filter Iterator adapter: filter, map, lalu collect() Iterator
Generator dan yield Tipe buatanmu sendiri yang mengimplementasikan Iterator, atau std::iter::from_fn Iterator kustom
with open(...) as f: Tidak perlu menulis apa-apa: file dan lock tertutup sendiri saat pemiliknya keluar dari scope (Drop) Scope, Membaca dan menulis file
Argumen default, *args, **kwargs Tidak didukung. Pakai parameter Option, slice (&[T]), struct dengan Default, atau builder Fungsi, Design pattern
Decorator Tidak ada padanan langsung. Bungkus dengan closure, atau pakai attribute macro (seperti #[tokio::main]) Closure, Makro prosedural
import dan modul mod dan use Modul
Package di PyPI Crate di crates.io Cargo dan crate
if __name__ == "__main__": fn main() Mulai dengan Rust
threading (dengan GIL) std::thread: thread paralel sungguhan Konkurensi
multiprocessing, concurrent.futures Thread, atau crate rayon untuk iterator paralel Konkurensi, Shared state
asyncio, async def, await async fn dan .await, dengan runtime seperti tokio Async/Await
7 // 2, 2 ** 10 7 / 2 (pembagian integer kalau keduanya integer), 2_i64.pow(10) Operator
int("42"), str(42) "42".parse::<i32>() (mengembalikan Result), 42.to_string() Konversi tipe, Konversi

Semua koleksi bawaan Python punya tempat di standard library Rust. Perbedaan terbesarnya: koleksi Rust hanya menampung satu tipe (Vec<i32> hanya berisi i32), dan HashMap tidak mengingat urutan penyisipan.

Python Rust Pelajari lebih lanjut
list Vec<T> Vektor
tuple Tuple (i32, String), atau struct kecil dengan field bernama Tuple
dict HashMap<K, V> (urutannya tidak tetap!) HashMap
dict yang key-nya perlu terurut BTreeMap<K, V> HashMap
dict yang perlu mengingat urutan penyisipan Crate indexmap
set, frozenset HashSet<T>, BTreeSet<T> HashSet
collections.Counter, defaultdict HashMap dengan entry API: *map.entry(k).or_insert(0) += 1 HashMap
collections.deque VecDeque<T> VecDeque dan BinaryHeap
heapq (yang terkecil duluan) BinaryHeap<T> (yang terbesar duluan; bungkus dengan Reverse agar yang terkecil duluan) VecDeque dan BinaryHeap
namedtuple struct Struct
bytes, bytearray &[u8], Vec<u8> Slice
Slicing: xs[1:3] Slice: &xs[1..3] (tidak ada indeks negatif; pakai xs.len() - 1) Slice

Di bagian inilah banyak developer Python bisa sedikit bernapas lega. Rust punya satu tool resmi, Cargo, yang mengerjakan tugas pip, venv, setuptools, pytest, dan lainnya. Tidak ada perdebatan “tool packaging mana yang sebaiknya kita pakai?”.

Python Rust Catatan
Interpreter python Compiler rustc, biasanya dipanggil lewat cargo Kamu compile dulu, lalu menjalankan binary native. Lihat Mulai dengan Rust
pyenv rustup (dan file rust-toolchain.toml untuk mengunci versi) Lihat Tools dan setup Rust
pip, poetry, uv cargo Mem-build, menjalankan, mengetes, menambah package, mem-publish. Lihat Cargo dan crate
pip install requests cargo add reqwest Menambahkan crate ke Cargo.toml, hanya untuk proyek ini
pyproject.toml / requirements.txt Cargo.toml
poetry.lock / uv.lock Cargo.lock Dibuat otomatis untukmu
venv / virtualenv Tidak perlu Setiap proyek punya dependency sendiri dan di-build ke folder target/ miliknya sendiri
PyPI crates.io Dokumentasi API setiap crate yang dipublikasikan ada di docs.rs
python main.py cargo run (cargo run --release untuk kecepatan penuh) Build debug jauh lebih lambat saat dijalankan; ukur kecepatan dalam mode release
pipx install ruff cargo install ripgrep Menginstal tool command-line
PyInstaller cargo build --release Hasilnya satu binary native; pengguna tidak perlu menginstal Rust
black cargo fmt Satu gaya standar untuk semua orang
ruff, pylint, flake8 cargo clippy Ratusan lint yang membantu, resmi
mypy, pyright Compiler-nya sendiri Pengecekan tipe tidak opsional dan bukan langkah terpisah
pytest, unittest #[test] bawaan dan cargo test (cargo nextest adalah runner populer yang lebih cepat) Lihat Testing
Fixture pytest dan parametrize rstest
hypothesis proptest Property-based testing. Lihat Pengujian lanjutan
unittest.mock mockall, atau fake buatan sendiri yang mengimplementasikan trait-mu
timeit, pytest-benchmark criterion Lihat Performa
cProfile, py-spy cargo flamegraph, perf, atau profiler native apa saja
Docstring + Sphinx Doc comment /// dalam Markdown + cargo doc Contoh kode di dokumentasi dijalankan sebagai test. Lihat Dokumentasi
REPL Python, Jupyter Rust Playground; evcxr_repl dan evcxr_jupyter Rust tidak punya REPL bawaan. Proyek kecil dengan cargo run adalah cara yang biasa untuk bereksperimen
VS Code + Pylance, PyCharm VS Code + rust-analyzer, atau JetBrains RustRover rust-analyzer memberi completion, go-to-definition, dan error langsung di baris kode

Ini contoh Cargo.toml kecil, supaya kamu bisa membandingkannya dengan pyproject.toml:

[package]
name = "weather-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.13", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }

Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?”

Section titled “Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?””

“Aku pakai requests, FastAPI, dan pandas. Versi Rust-nya apa?” Berikut padanan yang paling dekat. Ekosistem Python jauh lebih tua dan lebih besar, jadi di beberapa bidang (terutama data science) pilihan di Rust lebih sedikit dan lebih muda.

Python Rust Catatan
Flask, FastAPI axum (kita membuatnya di Proyek: Web API dengan Axum), actix-web Handler-nya berupa async fn biasa; tipe request dan response berasal dari struct-mu, agak mirip FastAPI dengan pydantic
Django Tidak ada padanan yang lengkap. Loco (loco-rs) adalah framework yang serba ada; kebanyakan tim menggabungkan axum + ORM + template engine Tidak ada situs admin bawaan
requests, httpx reqwest (async, dengan client blocking opsional), ureq (sederhana dan blocking) Lihat Jaringan di Rust
aiohttp reqwest + tokio
Jinja2 minijinja (dari pembuat Jinja sendiri), tera, askama (template dicek saat compile)
BeautifulSoup scraper
Python Rust Catatan
ORM SQLAlchemy Diesel (query dicek tipenya saat compile), SeaORM (async, gaya entity) Database di Rust menunjukkan keduanya dengan kode yang benar-benar jalan
psycopg, SQL mentah sqlx: SQL biasa, dicek terhadap database sungguhan saat compile Async, sangat populer, bukan ORM
sqlite3 rusqlite
redis-py redis
Celery apalis untuk antrean job; tokio::spawn untuk background task di dalam proses
Python Rust Catatan
pandas Polars Ditulis dengan Rust; API Python-nya, yang mungkin sudah kamu kenal, dibangun di atas library Rust yang sama
NumPy ndarray Array N-dimensi dengan slicing dan broadcasting; fitur tambahannya jauh lebih sedikit daripada NumPy
scikit-learn linfa, smartcore Algoritmanya lebih sedikit dan komunitasnya jauh lebih kecil
PyTorch candle (crate candle-core), burn Bagus untuk menjalankan model; sebagian besar training masih dilakukan di Python
matplotlib plotters
Modul csv Crate csv + serde
Python Rust Catatan
json serde + serde_json Standarnya; juga bisa TOML, YAML, dan lainnya. Lihat JSON dengan Serde
pydantic serde untuk parsing + validator atau garde untuk aturan validasi Struct-mu adalah skemanya
argparse, click, typer clap Definisikan argumenmu sebagai struct. Lihat Proyek: CLI dengan clap
logging tracing, atau log + env_logger Lihat Logging
re regex Tidak ada look-around atau backreference, tapi dijamin berjalan dalam waktu linear; fancy-regex punya keduanya. Lihat Regular expression
datetime chrono atau jiff Lihat Tanggal dan waktu
random rand Lihat Angka acak
asyncio tokio Lihat Async/Await
multiprocessing.Pool.map rayon: ganti .iter() dengan .par_iter()
itertools Sebagian besar sudah ada di Iterator; crate itertools menambahkan sisanya
os, pathlib, subprocess std::env, std::path, std::fs, std::process Lihat Membaca dan menulis file, Environment dan proses

Ini sebuah class Python kecil:

class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def __str__(self):
return f"{self.owner} has {self.balance}"
account = Account("Ada", 100)
account.deposit(50)
print(account)

Dan hal yang sama di Rust:

use std::fmt;
struct Account {
owner: String,
balance: i64,
}
impl Account {
// `new` hanya kebiasaan, bukan keyword. Tidak ada argumen default,
// jadi pemanggil selalu memberikan kedua nilainya.
fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// `&mut self`: method ini mengubah rekeningnya.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
// Inilah __str__ versi Rust.
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} punya {}", self.owner, self.balance)
}
}
fn main() {
let mut account = Account::new("Ada", 100);
account.deposit(50);
println!("{account}");
}

Output:

Ada punya 150
▶ Coba sendiri

Ada beberapa hal yang berbeda. Field-nya dideklarasikan di awal beserta tipenya, jadi kamu tidak bisa menambahkan atribut baru belakangan seperti account.nickname = "A" di Python. Data (struct) dan perilaku (impl) tinggal di dua blok yang terpisah. Setiap method menyatakan apakah ia hanya membaca (&self) atau mengubah (&mut self) nilainya, dan karena deposit mengubah rekening, variabelnya harus dideklarasikan dengan let mut. Rust tidak punya argumen default; kalau kamu ingin rekening yang dimulai dari nol, tambahkan fungsi kedua, misalnya fn empty(owner: &str) -> Self.

Python tidak peduli sebuah objek berasal dari class apa, yang penting ia punya method yang tepat:

class Duck:
def speak(self):
return "Quack"
class Robot:
def speak(self):
return "Beep boop"
def introduce(thing):
print(f"It says: {thing.speak()}")
for thing in [Duck(), Robot()]:
introduce(thing)
introduce(42) # AttributeError, tapi baru saat baris ini dijalankan

Di Rust, “punya method speak” ditulis sebagai sebuah trait, dan setiap tipe ikut serta lewat blok impl:

trait Speak {
fn speak(&self) -> String;
}
struct Duck;
struct Robot;
impl Speak for Duck {
fn speak(&self) -> String {
"Quack".to_string()
}
}
impl Speak for Robot {
fn speak(&self) -> String {
"Beep boop".to_string()
}
}
// Menerima tipe apa pun yang mengimplementasikan Speak.
fn introduce(thing: &dyn Speak) {
println!("Katanya: {}", thing.speak());
}
fn main() {
let things: Vec<Box<dyn Speak>> = vec![Box::new(Duck), Box::new(Robot)];
for thing in &things {
introduce(thing.as_ref());
}
// introduce(&42); // error compile: `{integer}` tidak mengimplementasikan Speak
}

Output:

Katanya: Quack
Katanya: Beep boop
▶ Coba sendiri

Jiwanya masih duck typing, tapi dicek oleh compiler: introduce(&42) sudah menjadi error sebelum program dijalankan, bukan AttributeError di production. Kalau kamu pernah memakai typing.Protocol, trait akan terasa wajar. List berisi tipe campuran butuh Box<dyn Speak>, karena Rust harus tahu cara memanggil method pada setiap item (lihat Trait object). Dan kamu bisa mengimplementasikan trait buatanmu untuk tipe yang bukan buatanmu, seperti String atau i32.

Di Python, nilai apa pun diam-diam bisa berisi None, dan kamu baru tahu saat memakainya:

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
return emails.get(user_id) # mengembalikan None kalau tidak ada
print(find_email(1).upper())
print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'

Di Rust, nilai yang mungkin tidak ada bertipe Option<T>. Kamu tidak bisa memakainya sebagai nilai biasa sebelum menangani kasus None:

fn find_email(user_id: u32) -> Option<String> {
match user_id {
1 => Some("[email protected]".to_string()),
2 => Some("[email protected]".to_string()),
_ => None,
}
}
fn main() {
for id in [1, 3] {
match find_email(id) {
Some(email) => println!("{id}: {}", email.to_uppercase()),
None => println!("{id}: email tidak terdaftar"),
}
}
// Seperti `find_email(3) or "unknown"` di Python.
let email = find_email(3).unwrap_or("tidak diketahui".to_string());
println!("{email}");
// Ubah nilai di dalamnya, kalau ada.
let upper = find_email(2).map(|e| e.to_uppercase());
println!("{upper:?}");
}

Output:

3: email tidak terdaftar
tidak diketahui
▶ Coba sendiri

Compiler Rust tidak akan membiarkanmu memanggil .to_uppercase() pada Option<String>, jadi error “'NoneType' object has no attribute” memang tidak mungkin terjadi. String di Rust selalu berisi string sungguhan. Pelajari method-method pembantunya di halaman Option, dan bentuk singkat if let Some(email) = ... di If Let / While Let.

Berdampingan: exception menjadi Result dan ?

Section titled “Berdampingan: exception menjadi Result dan ?”

Di Python, int() melempar exception, dan kamu menangkapnya di suatu tempat di atas call stack:

def parse_port(text):
port = int(text) # bisa melempar ValueError
if not 1 <= port <= 65535:
raise ValueError(f"port {port} is out of range")
return port
for text in ["8080", "http", "70000"]:
try:
print(f"port {parse_port(text)}")
except ValueError as e:
print(f"error: {e}")

Di Rust, fungsi yang bisa gagal menyatakannya di tipe kembaliannya. Operator ? berarti “kalau ini gagal, kembalikan error-nya ke pemanggilku sekarang juga”. Fungsinya sama seperti membiarkan exception naik ke atas, tapi kamu bisa melihatnya di kode:

fn parse_port(text: &str) -> Result<u32, String> {
let port: u32 = text
.parse()
.map_err(|e| format!("{text:?} bukan angka ({e})"))?;
if !(1..=65535).contains(&port) {
return Err(format!("port {port} di luar rentang"));
}
Ok(port)
}
fn main() {
for text in ["8080", "http", "70000"] {
match parse_port(text) {
Ok(port) => println!("port {port}"),
Err(e) => println!("error: {e}"),
}
}
}

Output:

port 8080
error: "http" bukan angka (invalid digit found in string)
error: port 70000 di luar rentang
▶ Coba sendiri

Pesan invalid digit found in string berasal dari standard library Rust, jadi tetap dalam bahasa Inggris. Artinya: ada karakter yang bukan angka di dalam string.

Perbedaan besarnya: di Python, fungsi apa pun bisa melempar exception, dan signature-nya tidak memberitahumu. Di Rust, Result adalah bagian dari signature, jadi pemanggil tidak bisa melupakannya. String sudah cukup sebagai tipe error selama kamu belajar; proyek sungguhan memakai enum error sendiri (Error kustom) atau crate anyhow, yang terasa mirip “lempar exception apa saja” (anyhow dan thiserror).

Berdampingan: comprehension dan dict menjadi iterator dan HashMap

Section titled “Berdampingan: comprehension dan dict menjadi iterator dan HashMap”

List comprehension dan menghitung dengan dict adalah Python sehari-hari:

text = "the cat sat on the mat the end"
loud = [w.upper() for w in text.split() if len(w) == 3]
print(loud)
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:3]
for word, n in top:
print(f"{word}: {n}")

Di Rust, comprehension menjadi rantai iterator yang diakhiri dengan collect(), dan dict menjadi HashMap:

use std::collections::HashMap;
fn main() {
let text = "the cat sat on the mat the end";
// [w.upper() for w in text.split() if len(w) == 3]
let loud: Vec<String> = text
.split_whitespace()
.filter(|w| w.len() == 3)
.map(|w| w.to_uppercase())
.collect();
println!("{loud:?}");
// counts[word] = counts.get(word, 0) + 1
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
// Yang paling sering duluan; kalau seri, urut abjad.
let mut top: Vec<(&str, u32)> = counts.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in top.iter().take(3) {
println!("{word}: {n}");
}
}

Output:

["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]
the: 3
cat: 1
end: 1
▶ Coba sendiri

Kalimat contohnya sengaja dibiarkan dalam bahasa Inggris: kode ini menyaring kata berdasarkan panjangnya dan mengurutkannya menurut abjad, jadi kata-kata lain akan memberi output yang berbeda.

Rantainya dibaca dari kiri ke kanan, bukan dari dalam ke luar, tapi idenya sama. Seperti generator expression, rantai ini lazy: tidak ada yang terjadi sampai collect(), sum(), count(), atau perulangan for menarik nilai-nilainya. collect() perlu tahu apa yang harus dibuat, karena itulah loud bertipe Vec<String>. Perhatikan juga langkah pengurutannya: dict di Python mengingat urutan penyisipan, tapi HashMap di Rust tidak, jadi kita selalu mengurutkan dulu sebelum mencetak. Pengurutan dibahas di halaman Mengurutkan dan mencari.

Di CPython standar, Global Interpreter Lock (GIL) hanya mengizinkan satu thread menjalankan bytecode Python pada satu waktu. Thread membantu saat kamu menunggu jaringan, tapi pekerjaan berat di CPU seperti ini tidak menjadi lebih cepat dengan menambah thread, jadi developer Python memilih multiprocessing:

from concurrent.futures import ThreadPoolExecutor
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
def count_primes(lo, hi):
return sum(1 for n in range(lo, hi) if is_prime(n))
ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)]
with ThreadPoolExecutor(4) as pool:
counts = list(pool.map(lambda r: count_primes(*r), ranges))
print("total:", sum(counts))

Rust tidak punya GIL. Setiap thread::spawn adalah thread sistem operasi sungguhan, dan semuanya berjalan bersamaan di core CPU yang berbeda:

use std::thread;
fn is_prime(n: u32) -> bool {
if n < 2 {
return false;
}
let mut d = 2;
while d * d <= n {
if n % d == 0 {
return false;
}
d += 1;
}
true
}
fn count_primes(lo: u32, hi: u32) -> usize {
(lo..hi).filter(|&n| is_prime(n)).count()
}
fn main() {
let ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)];
// Setiap rentang mendapat thread OS sendiri. Semuanya benar-benar berjalan bersamaan.
let counts: Vec<usize> = thread::scope(|s| {
let handles: Vec<_> = ranges
.iter()
.map(|&(lo, hi)| s.spawn(move || count_primes(lo, hi)))
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
for ((lo, hi), count) in ranges.iter().zip(&counts) {
println!("{lo}..{hi}: {count} bilangan prima");
}
println!("total: {}", counts.iter().sum::<usize>());
}

Output:

0..25000: 2762 bilangan prima
25000..50000: 2371 bilangan prima
50000..75000: 2260 bilangan prima
75000..100000: 2199 bilangan prima
total: 9592
▶ Coba sendiri

thread::scope menunggu semua thread selesai sebelum ia kembali, jadi thread-thread itu aman meminjam data lokal. Tanpa GIL, mungkin kamu khawatir dua thread mengubah nilai yang sama secara bersamaan. Rust menangani itu saat compile: data bersama yang bisa diubah harus dibungkus dengan sesuatu seperti Arc<Mutex<T>>, kalau tidak programnya tidak mau di-compile (lihat Shared state). Di proyek sungguhan, crate rayon membuatnya lebih pendek lagi, agak mirip multiprocessing.Pool.map tapi dengan thread:

use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();

Python juga sedang berubah di sini: Python 3.13 menambahkan build “free-threaded” opsional tanpa GIL. Tapi CPython standar yang diinstal dan di-deploy kebanyakan orang masih punya GIL, dan banyak extension C belum siap untuk free threading.

Section titled “Menulis extension Python dengan Rust (PyO3 + maturin)”

Kamu tidak perlu meninggalkan Python untuk memakai Rust. PyO3 memungkinkanmu menulis modul Python dengan Rust, dan maturin mem-build-nya menjadi wheel biasa yang bisa diinstal dengan pip. Beginilah cara Polars, pydantic-core, dan banyak library Python cepat lainnya dibuat.

Siapkan proyeknya (di dalam virtual environment yang sudah aktif):

Terminal window
pip install maturin
maturin new --bindings pyo3 fastmath
cd fastmath

maturin menuliskan Cargo.toml dan pyproject.toml untukmu. Lalu taruh fungsi-fungsi Rust-mu di src/lib.rs:

use pyo3::prelude::*;
/// Modul Python yang ditulis dengan Rust.
#[pymodule]
mod fastmath {
use pyo3::prelude::*;
/// Menjumlahkan kuadrat dari sebuah list angka.
#[pyfunction]
fn sum_of_squares(numbers: Vec<i64>) -> i64 {
numbers.iter().map(|n| n * n).sum()
}
/// Menghitung jumlah kata dalam sebuah teks.
#[pyfunction]
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
}

Build ke dalam virtual environment-mu dengan maturin develop (tambahkan --release kalau kamu peduli soal kecepatan), lalu pakai dari Python seperti modul lainnya:

import fastmath
print(fastmath.sum_of_squares([1, 2, 3, 4])) # 30
print(fastmath.word_count("the quick brown fox")) # 4
fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integer

PyO3 mengonversi tipe untukmu: list Python berisi int menjadi Vec<i64>, str menjadi &str, dan doc comment menjadi docstring Python. Tipe yang salah berubah menjadi TypeError Python biasa. Saat kamu siap membagikannya, maturin build --release membuat wheel, dan maturin publish mengunggahnya ke PyPI. Aturan yang bagus: ukur dulu, lalu pindahkan hanya perulangan yang paling sibuk ke Rust. Berpindah antara Python dan Rust punya sedikit biaya, jadi satu pemanggilan yang mengerjakan banyak hal lebih baik daripada sejuta pemanggilan kecil.

Hal-hal yang akan membuatmu tersandung sebagai developer Python

Section titled “Hal-hal yang akan membuatmu tersandung sebagai developer Python”

Semua orang yang datang dari Python menabrak polisi tidur yang sama. Mengetahuinya lebih awal akan membantu.

Di Python, b = a memberimu dua nama untuk list yang sama. Di Rust, let b = a; memindahkan (move) ownership sebuah Vec atau String ke b, dan a tidak bisa dipakai lagi:

fn main() {
let a = vec![1, 2, 3];
let mut b = a;
b.push(4);
println!("{a:?}");
}
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();
| ++++++++

Compiler memberitahumu bahwa a sudah dipindahkan di baris 3 (“value moved here”), lalu kamu mencoba memakainya lagi di baris 5. Solusinya adalah meminjam (let b = &a;, kalau kamu hanya perlu membaca) atau sengaja membuat salinan sungguhan (a.clone()). Satu aturan ini, ownership, adalah ide baru yang paling penting untukmu. Baca halaman itu dan Borrowing pelan-pelan.

  • Integer punya ukuran. int di Python tidak pernah overflow. i32 di Rust berhenti di 2.147.483.647; melewatinya akan panic di build debug. Pilih i64 atau u64 untuk angka besar, dan lihat Integer overflow.
  • Pembagian dan sisa bagi bekerja secara berbeda. 7 / 2 hasilnya 3 di Rust, karena kedua sisinya integer (seperti // di Python). Tulis 7.0 / 2.0 untuk mendapatkan 3.5. Selain itu, -7 % 3 hasilnya -1 di Rust tapi 2 di Python; pakai (-7_i32).rem_euclid(3) untuk mendapatkan jawaban versi Python.
  • Tipe angka tidak boleh dicampur. let x: f64 = 2.5 * 3; tidak bisa di-compile, karena 3 adalah integer. Tulis 3.0, atau konversi dengan as f64 (lihat Konversi tipe).
  • String tidak bisa diakses dengan indeks. word[0] adalah error compile, karena string Rust berformat UTF-8 dan satu “karakter” bisa terdiri dari beberapa byte. Pakai word.chars().next() atau word.chars().nth(i). Lihat String.
  • String vs &str. Python punya satu tipe str. Rust punya dua: String memiliki teksnya, &str meminjamnya. Fungsi biasanya menerima &str, dan struct biasanya menyimpan String.
  • Variabel immutable secara default. Tulis let mut kalau sebuah nilai perlu berubah. Compiler akan memberitahumu kalau kamu lupa.
  • Kurung kurawal dan titik koma. Indentasi tidak berarti apa-apa bagi Rust; { }-lah yang penting. Dan baris terakhir sebuah fungsi tanpa titik koma adalah nilai kembaliannya: fn double(x: i32) -> i32 { x * 2 }.
  • Setiap if dan match harus lengkap dan tipenya benar. Tidak ada truthiness: if count tidak bisa di-compile, kamu harus menulis if count > 0. Sebuah match harus mencakup semua kasus.
  • HashMap tidak punya urutan. dict di Python mengingat urutan penyisipan; HashMap di Rust tidak. Urutkan dulu sebelum mencetak, pakai BTreeMap, atau tambahkan crate indexmap.
  • Ada tahap compile. Tidak ada REPL, dan clean build sebuah proyek dengan banyak crate butuh waktu. Pakai cargo check selagi kamu mengedit (ia melewati tahap pembuatan kode dan jauh lebih cepat), dan cargo run --release saat kamu mengukur kecepatan.
  • Borrow checker akan melawan. Pola yang biasa di Python, seperti banyak objek yang sama-sama memegang dan mengubah satu objek bersama, atau objek yang menunjuk balik ke parent-nya, butuh smart pointer atau desain yang berbeda. Bersiaplah untuk beberapa minggu yang bikin frustrasi. Setelahnya jauh lebih mudah.

Kamu tidak perlu membaca situs ini berurutan. Sebagai developer Python, rute ini memberi waktu lebih untuk bagian Rust yang paling berbeda dari yang sudah kamu tahu:

  1. Mulai dengan Rust dan Tools dan setup Rust: instal Rust dan jalankan cargo new.
  2. Variabel, Tipe data, dan Integer overflow: tipe adalah kebiasaan baru yang pertama.
  3. Fungsi, If…Else, dan Loop: cepat dibaca; perhatikan ekspresi dan titik koma yang tidak ditulis.
  4. Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
  5. String: String vs &str, dan kenapa string tidak bisa diakses dengan indeks.
  6. Struct dan Enum, lalu Match dan Pattern matching.
  7. Option dan Result, lalu Error kustom.
  8. Vektor, HashMap, dan HashSet.
  9. Closure dan Iterator: comprehension dan generator versimu.
  10. Trait, Generik, Trait yang sering dipakai, dan Trait object.
  11. Modul, Cargo dan crate, dan Testing.
  12. Lifetime dan Smart pointer.
  13. Konkurensi, Shared state, dan Async/Await.
  14. JSON dengan Serde, lalu bangun CLI dengan clap atau Web API.

Kedua latihan memberimu potongan kode Python. Porting kode itu ke Rust.

Porting kode Python ini. Petunjuk: untuk mencetak nama secara berurutan, kumpulkan ke dalam Vec<&str>, panggil .sort(), lalu .join(", "). Untuk rata-ratanya, konversi dengan as f64 sebelum membagi.

scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)
print("passed:", ", ".join(passed))
odd_squares = [n * n for n in range(1, 10) if n % 2 == 1]
print(odd_squares)
average = sum(scores.values()) / len(scores)
print(f"average: {average:.1f}")
Lihat solusi
use std::collections::HashMap;
fn main() {
let scores: HashMap<&str, u32> =
HashMap::from([("Ada", 92), ("Bob", 47), ("Cy", 75), ("Dee", 38)]);
let mut passed: Vec<&str> = scores
.iter()
.filter(|(_, score)| **score >= 50)
.map(|(name, _)| *name)
.collect();
passed.sort();
println!("lulus: {}", passed.join(", "));
let odd_squares: Vec<u32> = (1..10).filter(|n| n % 2 == 1).map(|n| n * n).collect();
println!("{odd_squares:?}");
let total: u32 = scores.values().sum();
let average = total as f64 / scores.len() as f64;
println!("rata-rata: {average:.1}");
}

Output:

lulus: Ada, Cy
[1, 9, 25, 49, 81]
rata-rata: 63.0
▶ Coba sendiri

Melakukan iterasi atas HashMap memberimu pasangan referensi, karena itulah kamu melihat **score dan *name: keduanya mengikuti referensi kembali ke nilainya. sort() di sini tidak opsional: tanpanya, nama-nama itu bisa keluar dalam urutan apa saja. Dan total as f64 diperlukan karena Rust tidak pernah diam-diam mengubah integer menjadi float.

Porting kode Python ini. Setiap baris seharusnya berformat nama: umur. Petunjuk: line.split_once(':') mengembalikan Option<(&str, &str)>; ubah menjadi Result dengan .ok_or(...), lalu pakai ?. Buat fungsimu mengembalikan Result<(String, u32), String>.

def parse_line(line):
if ":" not in line:
raise ValueError("missing ':'")
name, age = line.split(":", 1)
try:
return name.strip(), int(age.strip())
except ValueError as e:
raise ValueError(f"bad age: {e}")
ages = {}
for line in ["Ada: 36", "Grace 45", "Linus: forty"]:
try:
name, age = parse_line(line)
ages[name] = age
print(f"ok: {name} is {age}")
except ValueError as e:
print(f"skipped {line!r}: {e}")
print(len(ages), "saved")
Lihat solusi
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (name, age) = line.split_once(':').ok_or("tidak ada ':'")?;
let age: u32 = age.trim().parse().map_err(|e| format!("umur tidak valid: {e}"))?;
Ok((name.trim().to_string(), age))
}
fn main() {
let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: empat puluh"] {
match parse_line(line) {
Ok((name, age)) => {
println!("ok: {name} berumur {age}");
ages.insert(name, age);
}
Err(e) => println!("dilewati {line:?}: {e}"),
}
}
println!("{} tersimpan", ages.len());
}

Output:

ok: Ada berumur 36
dilewati "Grace 45": tidak ada ':'
dilewati "Linus: empat puluh": umur tidak valid: invalid digit found in string
1 tersimpan
▶ Coba sendiri

Kedua raise menjadi nilai Err, dan try/except menjadi match. Tanda ? setelah ok_or("tidak ada ':'") tetap berfungsi meskipun error-nya bertipe &str sementara fungsinya mengembalikan error String, karena ? mengonversi error-nya dengan From. Perhatikan urutan di dalam Ok: kita mencetak dulu, baru insert memindahkan name ke dalam map. Tukar kedua baris itu, dan compiler akan memberitahumu bahwa name sudah dipindahkan.

  • Banyak hal dari Python tetap terpakai: perulangan atas iterator, tuple, closure, match, dan pengolahan data ala comprehension (dalam bentuk rantai iterator).
  • Perubahan besarnya: tipe statis yang dicek sebelum program berjalan, ownership menggantikan garbage collector, Option menggantikan None, dan Result + ? menggantikan exception.
  • Class menjadi struct + impl, duck typing menjadi trait, dan blok with menjadi Drop yang otomatis. HashMap tidak punya urutan, dan integer punya ukuran tetap.
  • Rust tidak punya GIL: thread berjalan paralel, dan compiler mencegah data race.
  • Cargo menggantikan pip, venv, dan kawan-kawannya; cargo fmt, cargo clippy, dan cargo test menggantikan black, ruff, dan pytest.
  • Kamu bisa memakai kedua bahasa sekaligus: PyO3 dan maturin mengubah kode Rust menjadi modul Python.

Kuis singkat

1. Apa yang dipakai Rust sebagai pengganti None di Python?

2. Di Python, b = a memberi dua nama untuk list yang sama. Apa yang dilakukan let b = a; pada sebuah Vec di Rust?

3. Berapa hasil 7 / 2 di Rust, kalau kedua angkanya integer?

4. Kamu ingin mempercepat satu fungsi lambat di proyek Python dengan Rust. Tool apa yang kamu pakai?

Skor: 0 / 4

  • The Rust Programming Language (bahasa Inggris): buku resmi Rust. Bab 4 (ownership), 6 (enum dan Option), 9 (error), dan 13 (closure dan iterator) yang paling penting untuk developer Python.
  • PyO3 user guide (bahasa Inggris): semua tentang memanggil Rust dari Python (dan Python dari Rust).
  • Maturin user guide (bahasa Inggris): mem-build, mengetes, dan mem-publish package Python yang ditenagai Rust.
  • Rust by Example (bahasa Inggris): contoh-contoh pendek yang bisa dijalankan, cocok kalau kamu paling cepat belajar dengan membaca kode.
  • Are We Learning Yet? (bahasa Inggris): gambaran umum crate machine learning dan data di Rust.