Zum Inhalt springen

Rust für Python-Entwickler: ein praktischer Leitfaden

Wenn du Python kannst, kennst du schon viele Ideen von Rust. Rust hat for-Schleifen über Iteratoren, Tupel, Closures (wie die Lambdas in Python, nur nützlicher), ein match, das ähnlich funktioniert wie match in Python, und Iterator-Ketten, die dasselbe erledigen wie List Comprehensions. Rust teilt sogar ein Stück vom Geschmack von Python: einen einzigen offensichtlichen Weg, Code zu formatieren, ein freundliches Paket-Tool und Fehlermeldungen, die sich wirklich Mühe geben, dir zu helfen.

Neu ist vor allem der Compiler. Python prüft Typen, während dein Programm läuft. Rust prüft sie alle, bevor es läuft. Python räumt Speicher mit einem Garbage Collector auf. Rust nutzt Ownership (Eigentum) und Borrowing (Ausleihen), geprüft beim Kompilieren. Python hat None und Exceptions. Rust hat Option und Result. In diese drei Dinge fließt der Großteil deiner Lernzeit.

Warum lernen Python-Entwickler also Rust? Das sind die ehrlichen Gründe:

  • Geschwindigkeit. Rust wird zu nativem Maschinencode kompiliert. Rechenintensive Arbeit, die in reinem Python langsam ist (Parsen, Zahlen verarbeiten, große Schleifen), ist in Rust meist viel schneller – ganz ohne Tuning-Tricks.
  • Echte Threads. Im Standard-CPython lässt der GIL immer nur einen Thread gleichzeitig Python-Code ausführen. Rust-Threads laufen gleichzeitig auf allen CPU-Kernen, und der Compiler verhindert Data Races, bevor dein Programm läuft.
  • Fehler werden früher gefunden. Ein Tippfehler in einem Attributnamen, ein vergessenes None oder eine Funktion, die mit dem falschen Typ aufgerufen wird: In Python merkst du das erst, wenn diese Zeile läuft. In Rust kompiliert das Programm erst, wenn alles behoben ist.
  • Du musst dich nicht entscheiden. Mit PyO3 (auf Englisch) und maturin schreibst du den langsamen Teil eines Python-Projekts in Rust und importierst ihn mit import wie jedes andere Modul. Beliebte Python-Tools wie Polars, Ruff und uv sind in Rust geschrieben.

Wenn du lieber einen allgemeinen Vergleich von Rust mit Python, Go und C++ suchst, schau unter Rust vs. C++, Go und Python.

Nutze diese Tabelle wie einen Sprachführer. Die letzte Spalte verlinkt die Lektion auf dieser Seite, die die Rust-Seite erklärt.

Python Rust Mehr dazu
Dynamische Typen, geprüft zur Laufzeit Statische Typen, geprüft beim Kompilieren. Rust leitet die meisten selbst ab, du schreibst also weniger Typen, als du denkst Datentypen
Type Hints (def f(x: int) -> str) Typen in Funktionssignaturen sind Pflicht, und der Compiler setzt sie durch Funktionen
x = 5 let x = 5; (unveränderbar). let mut x = 5;, wenn du den Wert ändern willst Variablen
int (wächst so groß wie nötig) Ganzzahlen mit fester Größe: i32, i64, u64, usize, i128 … Sie können überlaufen Datentypen, Integer-Überlauf
float f64 (oder f32) Datentypen
str String (besessen, wachsend) und &str (eine geliehene Sicht auf Text) Strings
f-Strings: f"Hi {name}" format!("Hi {name}") und println!("Hi {name}") Ausgabe
print(repr(x)) / __repr__ println!("{x:?}") mit #[derive(Debug)] Ausgabe, Wichtige Traits
__str__ Display implementieren (dann funktioniert auch .to_string()) Wichtige Traits
__eq__, __hash__, __lt__ PartialEq, Eq, Hash, PartialOrd, Ord per derive Wichtige Traits
None, Optional[T] Option<T>: entweder Some(value) oder None Option
x or default x.unwrap_or(default) Option
raise, try/except Result<T, E> + der Operator ? Result, Eigene Fehlertypen
Eine Exception für einen echten Bug (assert, IndexError) panic! (beendet den Thread) Panic
class mit __init__ und Methoden struct für die Daten + ein impl-Block für die Methoden; new ist eine ganz normale Funktion Structs
@dataclass struct mit #[derive(Debug, Clone, PartialEq)] Structs
self &self (lesen), &mut self (ändern) oder self (Ownership übernehmen) Structs
Vererbung Keine Vererbung. Nutze Komposition (ein Struct in einem Struct) + Traits mit Standardmethoden Traits, Entwurfsmuster
Duck Typing, Protocol, ABC trait Traits
Eine Liste mit Objekten verschiedener Klassen Vec<Box<dyn Trait>> (ein Trait-Objekt) Trait-Objekte
TypeVar / generische Funktionen Generics mit Trait Bounds: fn largest<T: Ord>(items: &[T]) Generics
Enum-Klasse enum, bei dem jede Variante eigene Daten tragen kann Enums
match / case (ab 3.10) match (der Compiler prüft, ob du jeden Fall abgedeckt hast) Match, Pattern Matching
if my_list: (Truthiness) Sei explizit: if !my_list.is_empty(). In ein if gehört nur ein bool If/Else
for i in range(10) for i in 0..10 For-Schleifen
enumerate(xs), zip(a, b) xs.iter().enumerate(), a.iter().zip(b.iter()) Iteratoren
lambda x: x + 1 Closures: |x| x + 1 (und sie dürfen viele Zeilen lang sein) Closures
List Comprehensions, map, filter Iterator-Adapter: filter, map, danach collect() Iteratoren
Generatoren und yield Ein eigener Typ, der Iterator implementiert, oder std::iter::from_fn Eigene Iteratoren
with open(...) as f: Nichts zu schreiben: Dateien und Locks werden geschlossen, wenn ihr Besitzer seinen Gültigkeitsbereich verlässt (Drop) Gültigkeitsbereich, Dateien lesen und schreiben
Standardargumente, *args, **kwargs Gibt es nicht. Nutze Option-Parameter, Slices (&[T]), ein Struct mit Default oder einen Builder Funktionen, Entwurfsmuster
Decorators Keine direkte Entsprechung. Umschließe mit Closures oder nutze Attribut-Makros (wie #[tokio::main]) Closures, Prozedurale Makros
import und Module mod und use Module
Paket auf PyPI Crate auf crates.io Cargo und Crates
if __name__ == "__main__": fn main() Erste Schritte
threading (mit dem GIL) std::thread: echte parallele Threads Nebenläufigkeit
multiprocessing, concurrent.futures Threads oder das Crate rayon für parallele Iteratoren Nebenläufigkeit, Geteilter Zustand
asyncio, async def, await async fn und .await, mit einer Runtime wie tokio Async/Await
7 // 2, 2 ** 10 7 / 2 (Ganzzahldivision bei Ganzzahlen), 2_i64.pow(10) Operatoren
int("42"), str(42) "42".parse::<i32>() (gibt ein Result zurück), 42.to_string() Typumwandlung, Konvertierungen

Die eingebauten Collections von Python haben alle ein Zuhause in der Standardbibliothek von Rust. Die größten Unterschiede: Eine Collection in Rust enthält nur einen Typ (ein Vec<i32> enthält nur i32-Werte), und eine HashMap merkt sich die Einfügereihenfolge nicht.

Python Rust Mehr dazu
list Vec<T> Vektoren
tuple Tupel (i32, String) oder ein kleines struct mit benannten Feldern Tupel
dict HashMap<K, V> (keine feste Reihenfolge!) HashMap
dict, wenn du sortierte Schlüssel brauchst BTreeMap<K, V> HashMap
dict, wenn du die Einfügereihenfolge brauchst Das Crate indexmap
set, frozenset HashSet<T>, BTreeSet<T> HashSet
collections.Counter, defaultdict HashMap mit der Entry-API: *map.entry(k).or_insert(0) += 1 HashMap
collections.deque VecDeque<T> VecDeque und BinaryHeap
heapq (kleinstes Element zuerst) BinaryHeap<T> (größtes Element zuerst; mit Reverse umschlossen das kleinste zuerst) VecDeque und BinaryHeap
namedtuple struct Structs
bytes, bytearray &[u8], Vec<u8> Slices
Slicing: xs[1:3] Slices: &xs[1..3] (keine negativen Indizes; nutze xs.len() - 1) Slices

Hier atmen viele Python-Entwickler ein wenig auf. Rust hat ein einziges offizielles Tool, Cargo, das die Aufgaben von pip, venv, setuptools, pytest und mehr übernimmt. Die Debatte „Welches Packaging-Tool nehmen wir?“ gibt es nicht.

Python Rust Hinweise
python-Interpreter rustc-Compiler, meist über cargo aufgerufen Du kompilierst erst und startest dann eine native Binärdatei. Siehe Erste Schritte
pyenv rustup (und eine Datei rust-toolchain.toml, um eine Version festzulegen) Siehe Rust-Werkzeuge und Einrichtung
pip, poetry, uv cargo Baut, startet, testet, fügt Pakete hinzu, veröffentlicht. Siehe Cargo und Crates
pip install requests cargo add reqwest Fügt das Crate nur für dieses Projekt zur Cargo.toml hinzu
pyproject.toml / requirements.txt Cargo.toml
poetry.lock / uv.lock Cargo.lock Wird für dich erzeugt
venv / virtualenv Nicht nötig Jedes Projekt hat seine eigenen Abhängigkeiten und baut in seinen eigenen Ordner target/
PyPI crates.io Die API-Doku jedes veröffentlichten Crates steht auf docs.rs
python main.py cargo run (cargo run --release für volle Geschwindigkeit) Debug-Builds laufen deutlich langsamer; Geschwindigkeit immer im Release-Modus messen
pipx install ruff cargo install ripgrep Installiert ein Kommandozeilen-Tool
PyInstaller cargo build --release Du bekommst eine einzige native Binärdatei; deine Nutzer brauchen kein Rust
black cargo fmt Ein einheitlicher Stil für alle
ruff, pylint, flake8 cargo clippy Hunderte hilfreiche Lints, offiziell
mypy, pyright Der Compiler selbst Typprüfung ist nicht optional und kein eigener Schritt
pytest, unittest Eingebautes #[test] und cargo test (cargo nextest ist ein beliebter, schnellerer Runner) Siehe Testen
pytest-Fixtures und parametrize rstest
hypothesis proptest Property-based Testing. Siehe Testen für Fortgeschrittene
unittest.mock mockall oder handgeschriebene Fakes, die deinen Trait implementieren
timeit, pytest-benchmark criterion Siehe Performance
cProfile, py-spy cargo flamegraph, perf oder ein beliebiger nativer Profiler
Docstrings + Sphinx ///-Doc-Kommentare in Markdown + cargo doc Code-Beispiele in der Doku laufen als Tests. Siehe Dokumentation
Die Python-REPL, Jupyter Der Rust Playground; evcxr_repl und evcxr_jupyter Rust hat keine eingebaute REPL. Zum Ausprobieren nimmt man meist kleine cargo run-Projekte
VS Code + Pylance, PyCharm VS Code + rust-analyzer oder JetBrains RustRover rust-analyzer bietet Vervollständigung, Sprung zur Definition und Fehler direkt im Code

Hier ist eine kleine Cargo.toml, damit du sie mit einer pyproject.toml vergleichen kannst:

[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"] }

Bibliotheken und Frameworks: „Was nehmen wir statt …?“

Abschnitt betitelt „Bibliotheken und Frameworks: „Was nehmen wir statt …?““

„Ich nutze requests, FastAPI und pandas. Was gibt es dafür in Rust?“ Hier sind die nächsten Entsprechungen. Das Ökosystem von Python ist viel älter und größer. In manchen Bereichen (vor allem Data Science) hat Rust deshalb weniger und jüngere Optionen.

Python Rust Hinweise
Flask, FastAPI axum (wir bauen eine API in Projekt: Web-API mit Axum), actix-web Handler sind einfache async fns; Request- und Response-Typen kommen aus deinen Structs, ein bisschen wie FastAPI mit pydantic
Django Keine vollständige Entsprechung. Loco (loco-rs) ist ein Framework mit „Batterien inklusive“; die meisten Teams kombinieren axum + ein ORM + eine Template-Engine Keine eingebaute Admin-Oberfläche
requests, httpx reqwest (async, mit optionalem blockierendem Client), ureq (einfach und blockierend) Siehe Netzwerkprogrammierung
aiohttp reqwest + tokio
Jinja2 minijinja (vom Autor von Jinja selbst), tera, askama (Templates, die beim Kompilieren geprüft werden)
BeautifulSoup scraper
Python Rust Hinweise
SQLAlchemy ORM Diesel (Abfragen werden beim Kompilieren typgeprüft), SeaORM (async, im Entity-Stil) Datenbanken in Rust zeigt beide mit funktionierendem Code
psycopg, rohes SQL sqlx: einfaches SQL, beim Kompilieren gegen deine echte Datenbank geprüft Async, sehr beliebt, kein ORM
sqlite3 rusqlite
redis-py redis
Celery apalis für Job-Queues; tokio::spawn für Hintergrund-Tasks im selben Prozess
Python Rust Hinweise
pandas Polars In Rust geschrieben; die Python-API, die du vielleicht schon kennst, baut auf derselben Rust-Bibliothek auf
NumPy ndarray N-dimensionale Arrays mit Slicing und Broadcasting; deutlich weniger Extras als NumPy
scikit-learn linfa, smartcore Weniger Algorithmen und viel kleinere Communitys
PyTorch candle (Crate candle-core), burn Gut, um Modelle auszuführen; trainiert wird meist noch in Python
matplotlib plotters
Modul csv Crate csv + serde
Python Rust Hinweise
json serde + serde_json Der Standard; kann auch TOML, YAML und mehr. Siehe JSON mit Serde
pydantic serde zum Parsen + validator oder garde für Validierungsregeln Dein Struct ist das Schema
argparse, click, typer clap Du definierst deine Argumente als Struct. Siehe Projekt: CLI-Tool mit clap
logging tracing oder log + env_logger Siehe Logging
re regex Kein Look-around und keine Rückverweise, dafür garantiert lineare Laufzeit; fancy-regex kann beides. Siehe Reguläre Ausdrücke
datetime chrono oder jiff Siehe Datum und Uhrzeit
random rand Siehe Zufallszahlen
asyncio tokio Siehe Async/Await
multiprocessing.Pool.map rayon: aus .iter() wird .par_iter()
itertools Das meiste ist schon in Iterator eingebaut; das Crate itertools ergänzt den Rest
os, pathlib, subprocess std::env, std::path, std::fs, std::process Siehe Dateien lesen und schreiben, Umgebung und Prozesse

Im direkten Vergleich: aus einer Klasse wird struct + impl

Abschnitt betitelt „Im direkten Vergleich: aus einer Klasse wird struct + impl“

Hier ist eine kleine Python-Klasse:

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} hat {self.balance}"
account = Account("Ada", 100)
account.deposit(50)
print(account)

Und dasselbe in Rust:

use std::fmt;
struct Account {
owner: String,
balance: i64,
}
impl Account {
// `new` ist nur eine Konvention, kein Schlüsselwort. Es gibt keine
// Standardargumente, also übergibt der Aufrufer immer beide Werte.
fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// `&mut self`: Diese Methode verändert das Konto.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
// Das ist das __str__ von Rust.
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} hat {}", self.owner, self.balance)
}
}
fn main() {
let mut account = Account::new("Ada", 100);
account.deposit(50);
println!("{account}");
}

Ausgabe:

Ada hat 150
▶ Selbst ausprobieren

Ein paar Dinge sind anders. Die Felder werden vorab mit ihren Typen deklariert. Du kannst also später kein neues Attribut hinzufügen, so wie Python dir account.nickname = "A" erlaubt. Daten (das struct) und Verhalten (das impl) stehen in getrennten Blöcken. Jede Methode sagt, ob sie den Wert nur liest (&self) oder verändert (&mut self). Und weil deposit das Konto verändert, muss die Variable mit let mut angelegt sein. Rust hat keine Standardargumente. Wenn du ein Konto willst, das bei null startet, schreibst du eine zweite Funktion, etwa fn empty(owner: &str) -> Self.

Im direkten Vergleich: aus Duck Typing werden Traits

Abschnitt betitelt „Im direkten Vergleich: aus Duck Typing werden Traits“

Python ist egal, von welcher Klasse ein Objekt ist. Wichtig ist nur, dass es die richtige Methode hat:

class Duck:
def speak(self):
return "Quak"
class Robot:
def speak(self):
return "Piep piep"
def introduce(thing):
print(f"Es sagt: {thing.speak()}")
for thing in [Duck(), Robot()]:
introduce(thing)
introduce(42) # AttributeError, aber erst, wenn diese Zeile läuft

In Rust wird „hat eine Methode speak“ als Trait aufgeschrieben, und jeder Typ macht mit einem impl-Block mit:

trait Speak {
fn speak(&self) -> String;
}
struct Duck;
struct Robot;
impl Speak for Duck {
fn speak(&self) -> String {
"Quak".to_string()
}
}
impl Speak for Robot {
fn speak(&self) -> String {
"Piep piep".to_string()
}
}
// Nimmt jeden Typ an, der Speak implementiert.
fn introduce(thing: &dyn Speak) {
println!("Es sagt: {}", 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); // Compilerfehler: `{integer}` implementiert Speak nicht
}

Ausgabe:

Es sagt: Quak
Es sagt: Piep piep
▶ Selbst ausprobieren

Im Grunde ist das immer noch Duck Typing, nur vom Compiler geprüft: introduce(&42) ist ein Fehler, bevor das Programm überhaupt läuft – und kein AttributeError in Produktion. Wenn du typing.Protocol kennst, fühlen sich Traits ganz natürlich an. Eine Liste mit gemischten Typen braucht Box<dyn Speak>, weil Rust wissen muss, wie es die Methode für jedes Element aufruft (siehe Trait-Objekte). Und du kannst deinen eigenen Trait auch für Typen implementieren, die du nicht geschrieben hast, etwa String oder i32.

In Python kann jeder Wert heimlich None sein, und du merkst es erst, wenn du ihn benutzt:

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
return emails.get(user_id) # gibt None zurück, wenn der Eintrag fehlt
print(find_email(1).upper())
print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'

In Rust hat ein Wert, der fehlen könnte, den Typ Option<T>. Du kannst ihn erst wie einen normalen Wert benutzen, wenn du den Fall None behandelt hast:

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}: keine E-Mail hinterlegt"),
}
}
// Wie `find_email(3) or "unbekannt"` in Python.
let email = find_email(3).unwrap_or("unbekannt".to_string());
println!("{email}");
// Den Wert darin verändern, falls es einen gibt.
let upper = find_email(2).map(|e| e.to_uppercase());
println!("{upper:?}");
}

Ausgabe:

3: keine E-Mail hinterlegt
unbekannt
▶ Selbst ausprobieren

Der Rust-Compiler lässt dich .to_uppercase() nicht auf einer Option<String> aufrufen. Der Fehler „'NoneType' object has no attribute“ kann also einfach nicht passieren. Ein String in Rust ist immer ein echter String. Die Hilfsmethoden lernst du auf der Seite Option kennen, die Kurzform if let Some(email) = ... unter If Let / While Let.

Im direkten Vergleich: aus Exceptions werden Result und ?

Abschnitt betitelt „Im direkten Vergleich: aus Exceptions werden Result und ?“

In Python wirft int() eine Exception, und du fängst sie irgendwo weiter oben im Call Stack ab:

def parse_port(text):
port = int(text) # kann ValueError werfen
if not 1 <= port <= 65535:
raise ValueError(f"Port {port} liegt außerhalb des gültigen Bereichs")
return port
for text in ["8080", "http", "70000"]:
try:
print(f"Port {parse_port(text)}")
except ValueError as e:
print(f"Fehler: {e}")

In Rust sagt eine Funktion, die fehlschlagen kann, das in ihrem Rückgabetyp. Der Operator ? bedeutet: „Wenn das fehlgeschlagen ist, gib den Fehler sofort an meinen Aufrufer zurück.“ Er erledigt dieselbe Aufgabe, wie eine Exception nach oben durchzureichen – nur siehst du es im Code:

fn parse_port(text: &str) -> Result<u32, String> {
let port: u32 = text
.parse()
.map_err(|e| format!("{text:?} ist keine Zahl ({e})"))?;
if !(1..=65535).contains(&port) {
return Err(format!("Port {port} liegt außerhalb des gültigen Bereichs"));
}
Ok(port)
}
fn main() {
for text in ["8080", "http", "70000"] {
match parse_port(text) {
Ok(port) => println!("Port {port}"),
Err(e) => println!("Fehler: {e}"),
}
}
}

Ausgabe:

Port 8080
Fehler: "http" ist keine Zahl (invalid digit found in string)
Fehler: Port 70000 liegt außerhalb des gültigen Bereichs
▶ Selbst ausprobieren

Die Meldung „invalid digit found in string“ kommt von Rust selbst und ist deshalb auf Englisch. Sie bedeutet: „ungültige Ziffer im String gefunden“.

Der große Unterschied: In Python kann jede Funktion eine Exception werfen, und nichts in ihrer Signatur verrät es dir. In Rust ist Result Teil der Signatur. Aufrufer können den Fehler also nicht vergessen. Beim Lernen ist String als Fehlertyp völlig in Ordnung. Echte Projekte nutzen eigene Fehler-Enums (Eigene Fehlertypen) oder das Crate anyhow, das sich sehr nach „irgendeine Exception werfen“ anfühlt (anyhow und thiserror).

Im direkten Vergleich: aus Comprehensions und Dicts werden Iteratoren und HashMap

Abschnitt betitelt „Im direkten Vergleich: aus Comprehensions und Dicts werden Iteratoren und HashMap“

List Comprehensions und Wörter zählen mit einem Dict sind Python-Alltag:

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}")

In Rust wird aus einer Comprehension eine Iterator-Kette, die mit collect() endet, und aus dem Dict wird eine 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;
}
// Häufigste zuerst; bei Gleichstand alphabetisch.
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}");
}
}

Ausgabe:

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

Der englische Beispielsatz bleibt hier absichtlich auf Englisch: Das Programm filtert nach der Wortlänge in Bytes und sortiert alphabetisch, und mit deutschen Wörtern (und Umlauten) käme etwas anderes heraus.

Die Kette liest sich von links nach rechts statt von innen nach außen, aber die Idee ist dieselbe. Wie ein Generator-Ausdruck ist sie lazy (sie arbeitet erst bei Bedarf): Nichts passiert, bis collect(), sum(), count() oder eine for-Schleife die Werte durchzieht. collect() muss wissen, was es bauen soll. Deshalb hat loud den Typ Vec<String>. Und achte auf den Sortierschritt: Ein dict in Python merkt sich die Einfügereihenfolge, eine HashMap in Rust nicht. Deshalb sortieren wir immer vor der Ausgabe. Mehr zum Sortieren steht auf der Seite Sortieren und Suchen.

Im Standard-CPython lässt der Global Interpreter Lock (GIL) immer nur einen Thread gleichzeitig Python-Bytecode ausführen. Threads helfen, wenn du auf das Netzwerk wartest. Rechenintensive Arbeit wie diese wird mit mehr Threads aber nicht schneller. Python-Entwickler greifen deshalb zu 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("insgesamt:", sum(counts))

Rust hat keinen GIL. Jedes thread::spawn ist ein echter Thread des Betriebssystems, und die Threads laufen gleichzeitig auf verschiedenen CPU-Kernen:

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)];
// Jeder Bereich bekommt seinen eigenen OS-Thread. Sie laufen wirklich gleichzeitig.
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} Primzahlen");
}
println!("insgesamt: {}", counts.iter().sum::<usize>());
}

Ausgabe:

0..25000: 2762 Primzahlen
25000..50000: 2371 Primzahlen
50000..75000: 2260 Primzahlen
75000..100000: 2199 Primzahlen
insgesamt: 9592
▶ Selbst ausprobieren

thread::scope wartet auf alle Threads, bevor es zurückkehrt. Deshalb dürfen die Threads lokale Daten gefahrlos ausleihen. Ohne GIL machst du dir vielleicht Sorgen, dass zwei Threads gleichzeitig denselben Wert ändern. Rust kümmert sich darum beim Kompilieren: Geteilte Daten, die sich ändern können, müssen in etwas wie Arc<Mutex<T>> stecken, sonst kompiliert das Programm nicht (siehe Geteilter Zustand). In echten Projekten macht das Crate rayon das noch kürzer, ein bisschen wie multiprocessing.Pool.map, nur mit Threads:

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

Auch Python ändert sich hier: Python 3.13 hat einen optionalen „free-threaded“ Build ohne GIL eingeführt. Das Standard-CPython, das die meisten installieren und ausliefern, hat aber weiterhin den GIL, und viele C-Erweiterungen sind noch nicht bereit für Free Threading.

Python-Erweiterungen in Rust schreiben (PyO3 + maturin)

Abschnitt betitelt „Python-Erweiterungen in Rust schreiben (PyO3 + maturin)“

Du musst Python nicht verlassen, um Rust zu nutzen. Mit PyO3 (auf Englisch) schreibst du ein Python-Modul in Rust, und maturin (auf Englisch) baut daraus ein normales Wheel, das pip installieren kann. So entstehen Polars, pydantic-core und viele andere schnelle Python-Bibliotheken.

Leg ein Projekt an (in einer aktivierten virtuellen Umgebung):

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

maturin schreibt die Cargo.toml und die pyproject.toml für dich. Danach kommen deine Rust-Funktionen in src/lib.rs:

use pyo3::prelude::*;
/// Ein Python-Modul, geschrieben in Rust.
#[pymodule]
mod fastmath {
use pyo3::prelude::*;
/// Addiert die Quadrate einer Liste von Zahlen.
#[pyfunction]
fn sum_of_squares(numbers: Vec<i64>) -> i64 {
numbers.iter().map(|n| n * n).sum()
}
/// Zählt die Wörter in einem Text.
#[pyfunction]
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
}

Bau es mit maturin develop in deine virtuelle Umgebung (mit --release, wenn dir Geschwindigkeit wichtig ist), und nutze es aus Python wie jedes andere Modul:

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 wandelt die Typen für dich um: Aus einer Python-list mit Ganzzahlen wird ein Vec<i64>, aus einem str ein &str, und aus den Doc-Kommentaren werden Python-Docstrings. Falsche Typen werden zu einem ganz normalen TypeError in Python. Wenn du es weitergeben willst, erzeugt maturin build --release ein Wheel, und maturin publish lädt es auf PyPI hoch. Eine gute Regel: Erst messen, dann nur die heiße Schleife nach Rust verschieben. Jeder Wechsel zwischen Python und Rust kostet ein wenig. Ein Aufruf, der viel Arbeit erledigt, ist deshalb besser als eine Million winziger Aufrufe.

Alle, die von Python kommen, stolpern über dieselben Dinge. Wenn du sie vorher kennst, hilft das.

In Python bekommst du mit b = a zwei Namen für dieselbe Liste. In Rust verschiebt let b = a; das Ownership an einem Vec oder String nach b (ein Move), und a ist danach nicht mehr benutzbar:

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();
| ++++++++

Der Compiler sagt auf Englisch: a wird ausgeliehen, nachdem sein Wert schon verschoben wurde. Er schlägt auch gleich vor, den Wert zu klonen. Die Lösung ist, auszuleihen (let b = &a;, wenn du nur lesen musst) oder bewusst eine echte Kopie anzulegen (a.clone()). Diese eine Regel, Ownership, ist die wichtigste neue Idee für dich. Lies diese Seite und Borrowing in Ruhe.

  • Ganzzahlen haben eine Größe. Ein int in Python läuft nie über. Ein i32 in Rust endet bei 2.147.483.647; wer darüber hinausgeht, löst in einem Debug-Build eine Panic aus. Nimm i64 oder u64 für große Zahlen und lies Integer-Überlauf.
  • Division und Rest funktionieren anders. 7 / 2 ergibt in Rust 3, weil beide Seiten Ganzzahlen sind (wie // in Python). Schreib 7.0 / 2.0, um 3.5 zu bekommen. Außerdem ist -7 % 3 in Rust -1, in Python aber 2. Mit (-7_i32).rem_euclid(3) bekommst du das Ergebnis von Python.
  • Keine gemischten Zahlentypen. let x: f64 = 2.5 * 3; kompiliert nicht, weil 3 eine Ganzzahl ist. Schreib 3.0 oder wandle mit as f64 um (siehe Typumwandlung).
  • Du kannst einen String nicht indizieren. word[0] ist ein Compilerfehler, weil Strings in Rust UTF-8 sind und ein „Zeichen“ mehrere Bytes lang sein kann. Nutze word.chars().next() oder word.chars().nth(i). Siehe Strings.
  • String vs. &str. Python hat einen einzigen str-Typ. Rust hat zwei: String besitzt seinen Text, &str leiht ihn aus. Funktionen nehmen meist &str entgegen, und Structs speichern meist String.
  • Variablen sind standardmäßig unveränderbar. Schreib let mut, wenn sich ein Wert ändern soll. Der Compiler sagt dir, wenn du es vergessen hast.
  • Klammern und Semikolons. Einrückung ist Rust egal, { } nicht. Und die letzte Zeile einer Funktion ohne Semikolon ist ihr Rückgabewert: fn double(x: i32) -> i32 { x * 2 }.
  • Jedes if und jedes match muss vollständig und typkorrekt sein. Keine Truthiness: if count kompiliert nicht, du schreibst if count > 0. Ein match muss jeden Fall abdecken.
  • Eine HashMap hat keine Reihenfolge. Ein dict in Python merkt sich die Einfügereihenfolge, eine HashMap in Rust nicht. Sortiere vor der Ausgabe, nimm eine BTreeMap oder füge das Crate indexmap hinzu.
  • Es gibt einen Kompilierschritt. Keine REPL, und ein kompletter Build eines Projekts mit vielen Crates dauert eine Weile. Nutze beim Bearbeiten cargo check (das überspringt die Codeerzeugung und ist viel schneller) und cargo run --release, wenn du Geschwindigkeit misst.
  • Der Borrow Checker hält dagegen. Muster, die in Python ganz normal sind, etwa viele Objekte, die alle ein gemeinsames Objekt halten und verändern, oder Objekte, die auf ihr Elternobjekt zurückzeigen, brauchen Smart Pointer oder ein anderes Design. Rechne mit ein paar frustrierenden Wochen. Danach wird es viel leichter.

Du musst diese Seite nicht der Reihe nach lesen. Als Python-Entwickler nimmst du dir auf diesem Weg Zeit für das, was in Rust am meisten anders ist als das, was du kennst:

  1. Erste Schritte mit Rust und Rust-Werkzeuge und Einrichtung: Rust installieren und cargo new ausführen.
  2. Variablen, Datentypen und Integer-Überlauf: Typen sind die erste neue Gewohnheit.
  3. Funktionen, If/Else und Schleifen: schnell gelesen; achte auf Ausdrücke und fehlende Semikolons.
  4. Ownership, dann Borrowing, dann Slices: die wichtigsten Seiten für dich. Lass dir Zeit.
  5. Strings: String vs. &str, und warum du einen String nicht indizieren kannst.
  6. Structs und Enums, dann Match und Pattern Matching.
  7. Option und Result, dann Eigene Fehlertypen.
  8. Vektoren, HashMap und HashSet.
  9. Closures und Iteratoren: deine Comprehensions und Generatoren.
  10. Traits, Generics, Wichtige Traits und Trait-Objekte.
  11. Module, Cargo und Crates und Testen.
  12. Lifetimes und Smart Pointer.
  13. Nebenläufigkeit, Geteilter Zustand und Async/Await.
  14. JSON mit Serde, danach baust du eine CLI mit clap oder eine Web-API.

Beide Übungen geben dir ein Stück Python-Code. Portiere es nach Rust.

Portiere diesen Python-Code. Tipp: Um die Namen sortiert auszugeben, sammle sie in einen Vec<&str>, ruf .sort() auf und danach .join(", "). Für den Durchschnitt wandelst du vor dem Teilen mit as f64 um.

scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)
print("bestanden:", ", ".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"Durchschnitt: {average:.1f}")
Lösung anzeigen
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!("bestanden: {}", 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!("Durchschnitt: {average:.1}");
}

Ausgabe:

bestanden: Ada, Cy
[1, 9, 25, 49, 81]
Durchschnitt: 63.0
▶ Selbst ausprobieren

Wenn du über eine HashMap iterierst, bekommst du Paare von Referenzen. Deshalb siehst du **score und *name: Sie folgen den Referenzen zurück zu den Werten. Das sort() ist hier nicht optional: Ohne es könnten die Namen in beliebiger Reihenfolge herauskommen. Und total as f64 ist nötig, weil Rust eine Ganzzahl nie hinter deinem Rücken in eine Gleitkommazahl verwandelt.

Portiere diesen Python-Code. Jede Zeile soll die Form Name: Alter haben. Tipp: line.split_once(':') gibt eine Option<(&str, &str)> zurück. Mach daraus mit .ok_or(...) ein Result und nutze dann ?. Deine Funktion soll Result<(String, u32), String> zurückgeben.

def parse_line(line):
if ":" not in line:
raise ValueError("':' fehlt")
name, age = line.split(":", 1)
try:
return name.strip(), int(age.strip())
except ValueError as e:
raise ValueError(f"ungültiges Alter: {e}")
ages = {}
for line in ["Ada: 36", "Grace 45", "Linus: vierzig"]:
try:
name, age = parse_line(line)
ages[name] = age
print(f"ok: {name} ist {age}")
except ValueError as e:
print(f"übersprungen {line!r}: {e}")
print(len(ages), "gespeichert")
Lösung anzeigen
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (name, age) = line.split_once(':').ok_or("':' fehlt")?;
let age: u32 = age.trim().parse().map_err(|e| format!("ungültiges Alter: {e}"))?;
Ok((name.trim().to_string(), age))
}
fn main() {
let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: vierzig"] {
match parse_line(line) {
Ok((name, age)) => {
println!("ok: {name} ist {age}");
ages.insert(name, age);
}
Err(e) => println!("übersprungen {line:?}: {e}"),
}
}
println!("{} gespeichert", ages.len());
}

Ausgabe:

ok: Ada ist 36
übersprungen "Grace 45": ':' fehlt
übersprungen "Linus: vierzig": ungültiges Alter: invalid digit found in string
1 gespeichert
▶ Selbst ausprobieren

Aus beiden raise-Anweisungen wurden Err-Werte, und aus try/except wurde ein match. Das ? nach ok_or("':' fehlt") funktioniert, obwohl der Fehler ein &str ist und die Funktion String-Fehler zurückgibt, weil ? den Fehler mit From umwandelt. Achte auf die Reihenfolge innerhalb von Ok: Wir geben zuerst aus, danach verschiebt insert den name in die Map. Vertausch die beiden Zeilen, und der Compiler sagt dir, dass name verschoben wurde.

  • Vieles aus Python nimmst du mit: Schleifen über Iteratoren, Tupel, Closures, match und Datenverarbeitung im Stil von Comprehensions (als Iterator-Ketten).
  • Die großen Umstellungen sind statische Typen, die vor dem Ausführen geprüft werden, Ownership statt Garbage Collector, Option statt None und Result + ? statt Exceptions.
  • Aus Klassen werden struct + impl, aus Duck Typing werden Traits, und aus with-Blöcken wird das automatische Drop. Eine HashMap hat keine Reihenfolge, und Ganzzahlen haben eine feste Größe.
  • Rust hat keinen GIL: Threads laufen parallel, und der Compiler verhindert Data Races.
  • Cargo ersetzt pip, venv und Co.; cargo fmt, cargo clippy und cargo test ersetzen black, ruff und pytest.
  • Du kannst beide Sprachen mischen: PyO3 und maturin machen aus Rust-Code ein Python-Modul.

Kurzer Check

1. Was nutzt Rust statt None aus Python?

2. In Python gibt dir b = a zwei Namen für dieselbe Liste. Was macht let b = a; mit einem Vec in Rust?

3. Was ergibt 7 / 2 in Rust, wenn beide Zahlen Ganzzahlen sind?

4. Du willst eine langsame Funktion in einem Python-Projekt mit Rust beschleunigen. Welche Tools nutzt du?

Punkte: 0 / 4

  • Die Programmiersprache Rust – Das Rust-Buch: die deutsche Übersetzung des offiziellen Buchs. Für Python-Entwickler sind die Kapitel 4 (Ownership), 6 (Enums und Option), 9 (Fehlerbehandlung) und 13 (Closures und Iteratoren) am wichtigsten.
  • PyO3 User Guide (auf Englisch): alles darüber, wie du Rust aus Python aufrufst (und Python aus Rust).
  • Maturin User Guide (auf Englisch): Python-Pakete mit Rust bauen, testen und veröffentlichen.
  • Rust by Example (auf Englisch): kurze, ausführbare Beispiele – gut, wenn du am besten lernst, indem du Code liest.
  • Are We Learning Yet? (auf Englisch): eine Übersicht über die Machine-Learning- und Daten-Crates von Rust.