Pular para o conteúdo

Rust para desenvolvedores Python: guia prático

Se você sabe Python, já conhece muitas das ideias do Rust. O Rust tem laços for sobre iteradores, tuplas, closures (as lambdas do Python, só que mais úteis), um match que funciona como o match do Python, e cadeias de iteradores que fazem o que as list comprehensions fazem. O Rust até pegou emprestado um pouco do gosto do Python: um jeito óbvio de formatar o código, uma ferramenta de pacotes amigável e mensagens de erro que se esforçam de verdade para ajudar você.

O que é novo tem a ver, principalmente, com o compilador. O Python verifica os tipos enquanto o programa roda; o Rust verifica todos antes de rodar. O Python limpa a memória com um garbage collector (coletor de lixo); o Rust usa ownership (posse) e borrowing (empréstimo), verificados em tempo de compilação. O Python tem None e exceções; o Rust tem Option e Result. É nessas três coisas que vai a maior parte do seu tempo de estudo.

Então, por que desenvolvedores Python aprendem Rust? Os motivos sinceros são estes:

  • Velocidade. O Rust compila para código de máquina nativo. Trabalho pesado de CPU que é lento em Python puro (parsing, cálculos numéricos, laços grandes) costuma ser muito mais rápido em Rust, sem truques de otimização.
  • Threads de verdade. No CPython padrão, o GIL deixa só uma thread rodar código Python por vez. As threads do Rust rodam em todos os núcleos da CPU ao mesmo tempo, e o compilador barra data races antes de o programa rodar.
  • Bugs encontrados mais cedo. Um erro de digitação no nome de um atributo, um None que você esqueceu, ou uma função chamada com o tipo errado: no Python, você descobre isso quando aquela linha roda. No Rust, o programa não compila até tudo estar corrigido.
  • Você não precisa escolher. Com o PyO3 e o maturin, você escreve a parte lenta de um projeto Python em Rust e faz import dela no Python como qualquer outro módulo. Ferramentas populares do Python, como Polars, Ruff e uv, são escritas em Rust.

Se você prefere uma comparação geral do Rust com Python, Go e C++, veja Rust vs outras linguagens.

Use esta tabela como um guia de conversação. A última coluna leva à lição deste site que explica o lado do Rust.

Python Rust Saiba mais
Tipos dinâmicos, verificados em tempo de execução Tipos estáticos, verificados em tempo de compilação. O Rust infere a maioria deles, então você escreve menos tipos do que imagina Tipos de dados
Type hints (def f(x: int) -> str) Os tipos na assinatura das funções são obrigatórios, e o compilador faz valer Funções
x = 5 let x = 5; (não muda). let mut x = 5; se você quiser mudar Variáveis
int (cresce o quanto for preciso) Inteiros de tamanho fixo: i32, i64, u64, usize, i128… Eles podem estourar (overflow) Tipos de dados, Overflow de inteiros
float f64 (ou f32) Tipos de dados
str String (com dono, pode crescer) e &str (uma visão emprestada de um texto) Strings
f-strings: f"Oi {name}" format!("Oi {name}") e println!("Oi {name}") Saída em Rust
print(repr(x)) / __repr__ println!("{x:?}") com #[derive(Debug)] Saída em Rust, Traits comuns
__str__ Implemente Display (e o .to_string() também passa a funcionar) Traits comuns
__eq__, __hash__, __lt__ Derive PartialEq, Eq, Hash, PartialOrd, Ord Traits comuns
None, Optional[T] Option<T>: ou Some(valor) ou None Option
x or default x.unwrap_or(default) Option
raise, try/except Result<T, E> + o operador ? Result, Erros personalizados
Uma exceção para um bug de verdade (assert, IndexError) panic! (encerra a thread) Panic
class com __init__ e métodos struct para os dados + um bloco impl para os métodos; new é uma função comum Structs
@dataclass struct com #[derive(Debug, Clone, PartialEq)] Structs
self &self (lê), &mut self (altera) ou self (toma o ownership) Structs
Herança Não existe herança. Use composição (uma struct dentro de outra) + traits com métodos padrão Traits, Padrões de projeto
Duck typing, Protocol, ABC trait Traits
Uma lista de objetos de classes diferentes Vec<Box<dyn Trait>> (um trait object) Trait objects
TypeVar / funções genéricas Genéricos com trait bounds: fn largest<T: Ord>(items: &[T]) Genéricos
Classe Enum enum em que cada variante pode carregar os próprios dados Enums
match / case (3.10+) match (o compilador confere se você tratou todos os casos) Match, Pattern matching
if my_list: (truthiness) Seja explícito: if !my_list.is_empty(). Só um bool entra num if If…Else
for i in range(10) for i in 0..10 Laços for
enumerate(xs), zip(a, b) xs.iter().enumerate(), a.iter().zip(b.iter()) Iteradores
lambda x: x + 1 Closures: |x| x + 1 (e elas podem ter várias linhas) Closures
List comprehensions, map, filter Adaptadores de iterador: filter, map e depois collect() Iteradores
Generators e yield Um tipo seu que implementa Iterator, ou std::iter::from_fn Iteradores personalizados
with open(...) as f: Nada para escrever: arquivos e locks são fechados quando o dono sai de escopo (Drop) Escopo, Arquivos
Argumentos padrão, *args, **kwargs Não existem. Use parâmetros Option, slices (&[T]), uma struct com Default ou um builder Funções, Padrões de projeto
Decorators Sem equivalente direto. Envolva com closures, ou use macros de atributo (como #[tokio::main]) Closures, Macros procedurais
import e módulos mod e use Módulos
Pacote no PyPI Crate no crates.io Cargo e crates
if __name__ == "__main__": fn main() Começando com Rust
threading (com o GIL) std::thread: threads paralelas de verdade Concorrência
multiprocessing, concurrent.futures Threads, ou o crate rayon para iteradores paralelos Concorrência, Estado compartilhado
asyncio, async def, await async fn e .await, com um runtime como o tokio Async/Await
7 // 2, 2 ** 10 7 / 2 (divisão inteira quando os dois lados são inteiros), 2_i64.pow(10) Operadores
int("42"), str(42) "42".parse::<i32>() (devolve um Result), 42.to_string() Conversão de tipos, Conversões

Todas as coleções embutidas do Python têm um lugar na biblioteca padrão do Rust. As maiores diferenças: uma coleção do Rust guarda um tipo só (um Vec<i32> guarda apenas i32), e um HashMap não lembra a ordem de inserção.

Python Rust Saiba mais
list Vec<T> Vetores
tuple Tuplas (i32, String), ou uma struct pequena com campos nomeados Tuplas
dict HashMap<K, V> (sem ordem fixa!) HashMap
dict quando você precisa das chaves ordenadas BTreeMap<K, V> HashMap
dict quando você precisa da ordem de inserção O crate indexmap
set, frozenset HashSet<T>, BTreeSet<T> HashSet
collections.Counter, defaultdict HashMap com a entry API: *map.entry(k).or_insert(0) += 1 HashMap
collections.deque VecDeque<T> VecDeque e BinaryHeap
heapq (o menor primeiro) BinaryHeap<T> (o maior primeiro; envolva os itens em Reverse para o menor primeiro) VecDeque e BinaryHeap
namedtuple struct Structs
bytes, bytearray &[u8], Vec<u8> Slices
Fatiamento: xs[1:3] Slices: &xs[1..3] (sem índices negativos; use xs.len() - 1) Slices

É aqui que muitos desenvolvedores Python respiram aliviados. O Rust tem uma ferramenta oficial, o Cargo, que faz o trabalho do pip, do venv, do setuptools, do pytest e de mais coisas. Não existe o debate “qual ferramenta de empacotamento vamos usar?”.

Python Rust Observações
Interpretador python Compilador rustc, normalmente chamado pelo cargo Primeiro você compila, depois roda um binário nativo. Veja Começando com Rust
pyenv rustup (e um arquivo rust-toolchain.toml para fixar uma versão) Veja Ferramentas do Rust
pip, poetry, uv cargo Compila, executa, testa, adiciona pacotes, publica. Veja Cargo e crates
pip install requests cargo add reqwest Adiciona o crate ao Cargo.toml só deste projeto
pyproject.toml / requirements.txt Cargo.toml
poetry.lock / uv.lock Cargo.lock Criado para você
venv / virtualenv Não é preciso Cada projeto tem as próprias dependências e compila para a própria pasta target/
PyPI crates.io A documentação da API de todo crate publicado fica no docs.rs
python main.py cargo run (cargo run --release para velocidade máxima) Builds de debug são bem mais lentos na execução; meça a velocidade em modo release
pipx install ruff cargo install ripgrep Instala uma ferramenta de linha de comando
PyInstaller cargo build --release Você recebe um único binário nativo; os usuários não precisam ter o Rust instalado
black cargo fmt Um estilo padrão para todo mundo
ruff, pylint, flake8 cargo clippy Centenas de lints úteis, oficial
mypy, pyright O próprio compilador A verificação de tipos não é opcional nem uma etapa separada
pytest, unittest #[test] embutido e cargo test (o cargo nextest é um executor popular e mais rápido) Veja Testes em Rust
Fixtures do pytest e parametrize rstest
hypothesis proptest Testes baseados em propriedades. Veja Testes avançados
unittest.mock mockall, ou fakes escritos à mão que implementam o seu trait
timeit, pytest-benchmark criterion Veja Desempenho
cProfile, py-spy cargo flamegraph, perf, ou qualquer profiler nativo
Docstrings + Sphinx Comentários de documentação /// em Markdown + cargo doc Os exemplos de código na documentação rodam como testes. Veja Documentação
O REPL do Python, Jupyter O Rust Playground; evcxr_repl e evcxr_jupyter O Rust não tem um REPL embutido. Projetos pequenos com cargo run são o jeito comum de experimentar
VS Code + Pylance, PyCharm VS Code + rust-analyzer, ou JetBrains RustRover O rust-analyzer oferece autocompletar, ir para a definição e erros na linha

Aqui está um Cargo.toml pequeno, para você comparar com um 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"] }

Bibliotecas e frameworks: “O que usamos no lugar de…?”

Seção intitulada “Bibliotecas e frameworks: “O que usamos no lugar de…?””

“Eu uso requests, FastAPI e pandas. Qual é a versão em Rust?” Aqui estão os equivalentes mais próximos. O ecossistema do Python é bem mais antigo e maior, então em algumas áreas (principalmente ciência de dados) o Rust tem menos opções, e mais jovens.

Python Rust Observações
Flask, FastAPI axum (construímos uma API com ele em Projeto: API web com Axum), actix-web Os handlers são async fn simples; os tipos de requisição e resposta vêm das suas structs, um pouco como o FastAPI com pydantic
Django Não há um equivalente completo. O Loco (loco-rs) é um framework que já vem com tudo; a maioria dos times combina axum + um ORM + um motor de templates Não existe um painel de administração embutido
requests, httpx reqwest (assíncrono, com um cliente bloqueante opcional), ureq (simples e bloqueante) Veja Redes em Rust
aiohttp reqwest + tokio
Jinja2 minijinja (do próprio autor do Jinja), tera, askama (templates verificados em tempo de compilação)
BeautifulSoup scraper
Python Rust Observações
ORM do SQLAlchemy Diesel (consultas com tipos verificados em tempo de compilação), SeaORM (assíncrono, no estilo de entidades) Bancos de dados em Rust mostra os dois com código que funciona
psycopg, SQL puro sqlx: SQL puro, conferido com o seu banco de dados real em tempo de compilação Assíncrono, muito popular, não é um ORM
sqlite3 rusqlite
redis-py redis
Celery apalis para filas de jobs; tokio::spawn para tarefas em segundo plano dentro do processo
Python Rust Observações
pandas Polars Escrito em Rust; a API Python dele, que você talvez já conheça, é construída sobre a mesma biblioteca Rust
NumPy ndarray Arrays N-dimensionais com fatiamento e broadcasting; bem menos extras que o NumPy
scikit-learn linfa, smartcore Menos algoritmos e comunidades bem menores
PyTorch candle (crate candle-core), burn Bons para rodar modelos; a maior parte do treinamento ainda acontece em Python
matplotlib plotters
Módulo csv Crate csv + serde
Python Rust Observações
json serde + serde_json O padrão; também lida com TOML, YAML e mais. Veja JSON com Serde
pydantic serde para o parsing + validator ou garde para as regras de validação A sua struct é o schema
argparse, click, typer clap Defina os argumentos como uma struct. Veja Projeto: CLI com clap
logging tracing, ou log + env_logger Veja Logging
re regex Sem lookaround nem backreferences, mas com tempo linear garantido; o fancy-regex tem os dois. Veja Expressões regulares
datetime chrono ou jiff Veja Datas e horas
random rand Veja Números aleatórios
asyncio tokio Veja Async/Await
multiprocessing.Pool.map rayon: troque .iter() por .par_iter()
itertools Quase tudo já vem no Iterator; o crate itertools adiciona o resto
os, pathlib, subprocess std::env, std::path, std::fs, std::process Veja Arquivos, Ambiente e processos

Aqui está uma pequena classe em Python:

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)

E a mesma coisa em Rust:

use std::fmt;
struct Account {
owner: String,
balance: i64,
}
impl Account {
// `new` é só uma convenção, não uma palavra-chave. Não existem argumentos
// padrão, então quem chama sempre passa os dois valores.
fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// `&mut self`: este método altera a conta.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
// Este é o __str__ do Rust.
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} tem {}", self.owner, self.balance)
}
}
fn main() {
let mut account = Account::new("Ada", 100);
account.deposit(50);
println!("{account}");
}

Saída:

Ada tem 150
▶ Experimente você mesmo

Algumas coisas mudam. Os campos são declarados logo de início, com os seus tipos, então você não pode inventar um atributo novo depois, como o Python deixa fazer com account.nickname = "A". Os dados (a struct) e o comportamento (o impl) ficam em blocos separados. Cada método diz se só lê (&self) ou altera (&mut self) o valor, e como deposit altera a conta, a variável precisa ser declarada com let mut. O Rust não tem argumentos padrão; se você quiser uma conta que começa em zero, crie uma segunda função, como fn empty(owner: &str) -> Self.

O Python não liga para a classe de um objeto, só para ele ter o método certo:

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, mas só quando esta linha roda

Em Rust, “tem um método speak” fica escrito como um trait, e cada tipo entra no clube com um bloco 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()
}
}
// Aceita qualquer tipo que implementa Speak.
fn introduce(thing: &dyn Speak) {
println!("Ele diz: {}", 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); // erro de compilação: `{integer}` não implementa Speak
}

Saída:

Ele diz: Quack
Ele diz: Beep boop
▶ Experimente você mesmo

No espírito, continua sendo duck typing, só que verificado pelo compilador: introduce(&42) é um erro antes mesmo de o programa rodar, e não um AttributeError em produção. Se você já usou typing.Protocol, os traits vão parecer naturais. Uma lista com tipos misturados precisa de Box<dyn Speak>, porque o Rust tem que saber como chamar o método em cada item (veja Trait objects). E você pode implementar um trait seu para tipos que você não escreveu, como String ou i32.

Em Python, qualquer valor pode ser None às escondidas, e você só descobre quando usa:

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
return emails.get(user_id) # devolve None se não existir
print(find_email(1).upper())
print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'

Em Rust, um valor que pode estar faltando tem o tipo Option<T>. Você não consegue usá-lo como um valor comum antes de tratar o caso 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}: nenhum e-mail cadastrado"),
}
}
// Como o `find_email(3) or "unknown"` do Python.
let email = find_email(3).unwrap_or("desconhecido".to_string());
println!("{email}");
// Altera o valor de dentro, se houver um.
let upper = find_email(2).map(|e| e.to_uppercase());
println!("{upper:?}");
}

Saída:

3: nenhum e-mail cadastrado
desconhecido
▶ Experimente você mesmo

O compilador do Rust não deixa você chamar .to_uppercase() em um Option<String>, então o erro “'NoneType' object has no attribute” simplesmente não tem como acontecer. Uma String do Rust é sempre uma string de verdade. Aprenda os métodos auxiliares na página de Option, e a forma curta if let Some(email) = ... em If Let / While Let.

Em Python, int() lança uma exceção, e você a captura em algum lugar mais acima na pilha de chamadas:

def parse_port(text):
port = int(text) # pode lançar 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}")

Em Rust, uma função que pode falhar diz isso no tipo de retorno. O operador ? significa “se isto falhou, devolva o erro para quem me chamou agora mesmo”. Ele faz o mesmo trabalho de deixar uma exceção subir, mas você consegue vê-lo no código:

fn parse_port(text: &str) -> Result<u32, String> {
let port: u32 = text
.parse()
.map_err(|e| format!("{text:?} não é um número ({e})"))?;
if !(1..=65535).contains(&port) {
return Err(format!("a porta {port} está fora do intervalo"));
}
Ok(port)
}
fn main() {
for text in ["8080", "http", "70000"] {
match parse_port(text) {
Ok(port) => println!("porta {port}"),
Err(e) => println!("erro: {e}"),
}
}
}

Saída:

porta 8080
erro: "http" não é um número (invalid digit found in string)
erro: a porta 70000 está fora do intervalo
▶ Experimente você mesmo

A mensagem “invalid digit found in string” (dígito inválido encontrado na string) continua em inglês porque ela vem da biblioteca padrão do Rust. A grande diferença: no Python, qualquer função pode lançar uma exceção, e nada na assinatura conta isso para você. No Rust, o Result faz parte da assinatura, então quem chama não tem como esquecer dele. String serve bem como tipo de erro enquanto você aprende; projetos de verdade usam enums de erro próprios (Erros personalizados) ou o crate anyhow, que lembra bastante o “lançar qualquer exceção” (anyhow e thiserror).

Lado a lado: comprehensions e dicts viram iteradores e HashMap

Seção intitulada “Lado a lado: comprehensions e dicts viram iteradores e HashMap”

List comprehensions e contagem com dict são o dia a dia do Python:

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

Em Rust, uma comprehension vira uma cadeia de iteradores que termina em collect(), e o dict vira um 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;
}
// As mais comuns primeiro; empates em ordem alfabética.
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}");
}
}

Saída:

["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]
the: 3
cat: 1
end: 1
▶ Experimente você mesmo

O texto ficou em inglês de propósito: o exemplo filtra palavras pelo tamanho em bytes (w.len() == 3) e ordena por ordem alfabética, e letras acentuadas mudariam os dois resultados. A cadeia se lê da esquerda para a direita, em vez de dentro para fora, mas a ideia é a mesma. Como uma generator expression, ela é preguiçosa (lazy): nada acontece até que collect(), sum(), count() ou um laço for puxe os valores. O collect() precisa saber o que construir, e é por isso que loud tem o tipo Vec<String>. E repare na etapa de ordenação: um dict do Python lembra a ordem de inserção, mas um HashMap do Rust não, então sempre ordenamos antes de imprimir. A ordenação é explicada na página Ordenação e busca.

No CPython padrão, o Global Interpreter Lock (GIL) deixa só uma thread executar bytecode Python por vez. Threads ajudam quando você espera pela rede, mas trabalho pesado de CPU como este não fica mais rápido com mais threads, então desenvolvedores Python recorrem ao 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))

O Rust não tem GIL. Cada thread::spawn é uma thread real do sistema operacional, e elas rodam em núcleos diferentes da CPU ao mesmo tempo:

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)];
// Cada intervalo ganha a própria thread do sistema. Elas rodam mesmo ao mesmo tempo.
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} primos");
}
println!("total: {}", counts.iter().sum::<usize>());
}

Saída:

0..25000: 2762 primos
25000..50000: 2371 primos
50000..75000: 2260 primos
75000..100000: 2199 primos
total: 9592
▶ Experimente você mesmo

O thread::scope espera todas as threads terminarem antes de retornar, então as threads podem pegar dados locais emprestados com segurança. Sem um GIL, você poderia se preocupar com duas threads alterando o mesmo valor ao mesmo tempo. O Rust cuida disso em tempo de compilação: dados compartilhados que podem mudar precisam ficar dentro de algo como Arc<Mutex<T>>, senão o programa não compila (veja Estado compartilhado). Em projetos de verdade, o crate rayon deixa isso ainda mais curto, um pouco como o multiprocessing.Pool.map, mas com threads:

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

O Python também está mudando nesse ponto: o Python 3.13 trouxe um build opcional “free-threaded”, sem o GIL. Mas o CPython padrão, que a maioria das pessoas instala e coloca em produção, ainda tem o GIL, e muitas extensões em C ainda não estão prontas para o free threading.

Escrevendo extensões Python em Rust (PyO3 + maturin)

Seção intitulada “Escrevendo extensões Python em Rust (PyO3 + maturin)”

Você não precisa largar o Python para usar Rust. O PyO3 permite escrever um módulo Python em Rust, e o maturin empacota esse módulo num wheel normal, que o pip consegue instalar. É assim que são feitos o Polars, o pydantic-core e muitas outras bibliotecas Python rápidas.

Crie um projeto (dentro de um ambiente virtual ativado):

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

O maturin escreve o Cargo.toml e o pyproject.toml para você. Depois, coloque as suas funções Rust em src/lib.rs:

use pyo3::prelude::*;
/// Um módulo Python escrito em Rust.
#[pymodule]
mod fastmath {
use pyo3::prelude::*;
/// Soma os quadrados de uma lista de números.
#[pyfunction]
fn sum_of_squares(numbers: Vec<i64>) -> i64 {
numbers.iter().map(|n| n * n).sum()
}
/// Conta as palavras de um texto.
#[pyfunction]
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
}

Compile o módulo dentro do seu ambiente virtual com maturin develop (acrescente --release quando a velocidade importar) e use-o no Python como qualquer outro módulo:

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

O PyO3 converte os tipos para você: uma list de inteiros do Python vira um Vec<i64>, uma str vira um &str, e os comentários de documentação viram docstrings do Python. Tipos errados viram um TypeError normal do Python. Quando estiver pronto para compartilhar, o maturin build --release gera um wheel, e o maturin publish envia para o PyPI. Uma boa regra: meça primeiro e depois leve para o Rust só o laço mais pesado. Atravessar a fronteira entre Python e Rust tem um pequeno custo, então uma chamada que faz muito trabalho é melhor que um milhão de chamadas minúsculas.

Todo mundo que vem do Python passa pelos mesmos buracos. Conhecer todos com antecedência ajuda.

Em Python, b = a dá dois nomes para a mesma lista. Em Rust, let b = a; move o ownership de um Vec ou de uma String para b, e a não pode mais ser usado:

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

O compilador diz (em inglês) que a foi movido na linha 3 (“value moved here”) e depois usado na linha 5 (“value borrowed here after move”), e até sugere um .clone(). A correção é pegar emprestado (let b = &a;, se você só precisa ler) ou fazer uma cópia de verdade, de propósito (a.clone()). Essa regra, o ownership, é a ideia nova mais importante para você. Leia essa página e a de Borrowing com calma.

  • Inteiros têm tamanho. O int do Python nunca estoura. Um i32 do Rust para em 2.147.483.647; passar disso causa um panic num build de debug. Escolha i64 ou u64 para números grandes, e veja Overflow de inteiros.
  • Divisão e resto funcionam diferente. 7 / 2 dá 3 em Rust, porque os dois lados são inteiros (o // do Python). Escreva 7.0 / 2.0 para ter 3.5. Além disso, -7 % 3 dá -1 em Rust, mas 2 em Python; use (-7_i32).rem_euclid(3) para ter a resposta do Python.
  • Nada de misturar tipos numéricos. let x: f64 = 2.5 * 3; não compila, porque 3 é um inteiro. Escreva 3.0, ou converta com as f64 (veja Conversão de tipos).
  • Você não consegue indexar uma string. word[0] é um erro de compilação, porque as strings do Rust são UTF-8 e um “caractere” pode ocupar vários bytes. Use word.chars().next() ou word.chars().nth(i). Veja Strings.
  • String vs &str. O Python tem um único tipo str. O Rust tem dois: String é dona do texto, &str pega o texto emprestado. Funções normalmente recebem &str e structs normalmente guardam String.
  • Variáveis são imutáveis por padrão. Escreva let mut quando um valor precisar mudar. O compilador avisa quando você esquecer.
  • Chaves e ponto e vírgula. A indentação não importa para o Rust; as { } importam. E a última linha de uma função sem ponto e vírgula é o valor de retorno: fn double(x: i32) -> i32 { x * 2 }.
  • Todo if e todo match precisam ser completos e ter os tipos certos. Não existe truthiness: if count não compila, você escreve if count > 0. Um match precisa cobrir todos os casos.
  • Um HashMap não tem ordem. O dict do Python lembra a ordem de inserção; o HashMap do Rust não. Ordene antes de imprimir, use um BTreeMap, ou adicione o crate indexmap.
  • Existe uma etapa de compilação. Não há REPL, e um build limpo de um projeto com muitos crates demora um pouco. Use o cargo check enquanto edita (ele pula a geração de código e é bem mais rápido), e o cargo run --release quando for medir velocidade.
  • O borrow checker vai bater de frente. Padrões normais no Python, como muitos objetos guardando e alterando um mesmo objeto compartilhado, ou objetos que apontam de volta para o pai, precisam de smart pointers (ponteiros inteligentes) ou de outro design. Prepare-se para algumas semanas frustrantes. Depois fica bem mais fácil.

Você não precisa ler este site na ordem. Como desenvolvedor Python, este caminho investe tempo onde o Rust é mais diferente do que você já conhece:

  1. Começando com Rust e Ferramentas do Rust: instale o Rust e rode cargo new.
  2. Variáveis, Tipos de dados e Overflow de inteiros: os tipos são o primeiro hábito novo.
  3. Funções, If…Else e Loops: leituras rápidas; repare nas expressões e nos pontos e vírgulas que faltam.
  4. Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
  5. Strings: String vs &str, e por que você não consegue indexar uma string.
  6. Structs e Enums, depois Match e Pattern matching.
  7. Option e Result, depois Erros personalizados.
  8. Vetores, HashMap e HashSet.
  9. Closures e Iteradores: as suas comprehensions e generators.
  10. Traits, Genéricos, Traits comuns e Trait objects.
  11. Módulos, Cargo e crates e Testes em Rust.
  12. Lifetimes e Ponteiros inteligentes.
  13. Concorrência, Estado compartilhado e Async/Await.
  14. JSON com Serde, e depois construa uma CLI com clap ou uma API web.

Os dois exercícios trazem um trecho de Python. Converta para Rust.

Converta este código Python. Dica: para imprimir os nomes em ordem, colete-os num Vec<&str>, chame .sort() e depois .join(", "). Para a média, converta com as f64 antes de dividir.

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}")
Ver solução
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!("aprovados: {}", 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!("média: {average:.1}");
}

Saída:

aprovados: Ada, Cy
[1, 9, 25, 49, 81]
média: 63.0
▶ Experimente você mesmo

Percorrer um HashMap dá pares de referências, e é por isso que aparecem **score e *name: eles seguem as referências até os valores. O sort() não é opcional aqui: sem ele, os nomes poderiam sair em qualquer ordem. E o total as f64 é necessário porque o Rust nunca transforma um inteiro em float pelas suas costas.

Converta este código Python. Cada linha deve ter o formato nome: idade. Dica: line.split_once(':') devolve um Option<(&str, &str)>; transforme em um Result com .ok_or(...) e depois use ?. Faça a sua função devolver 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")
Ver solução
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (name, age) = line.split_once(':').ok_or("falta o ':'")?;
let age: u32 = age.trim().parse().map_err(|e| format!("idade inválida: {e}"))?;
Ok((name.trim().to_string(), age))
}
fn main() {
let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: forty"] {
match parse_line(line) {
Ok((name, age)) => {
println!("ok: {name} tem {age} anos");
ages.insert(name, age);
}
Err(e) => println!("ignorada {line:?}: {e}"),
}
}
println!("{} salvo(s)", ages.len());
}

Saída:

ok: Ada tem 36 anos
ignorada "Grace 45": falta o ':'
ignorada "Linus: forty": idade inválida: invalid digit found in string
1 salvo(s)
▶ Experimente você mesmo

Os dois raise viraram valores Err, e o try/except virou um match. O ? depois de ok_or("falta o ':'") funciona mesmo com o erro sendo um &str e a função devolvendo erros do tipo String, porque o ? converte o erro com From. Repare na ordem dentro do Ok: primeiro imprimimos, depois o insert move name para dentro do mapa. Troque essas duas linhas de lugar e o compilador vai avisar que name foi movido.

  • Muita coisa do Python vem junto: laços sobre iteradores, tuplas, closures, match e o processamento de dados no estilo das comprehensions (como cadeias de iteradores).
  • As grandes mudanças são tipos estáticos verificados antes de rodar, ownership no lugar de um garbage collector, Option no lugar de None, e Result + ? no lugar de exceções.
  • Classes viram uma struct + impl, duck typing vira traits, e blocos with viram o Drop automático. Um HashMap não tem ordem, e inteiros têm tamanho fixo.
  • O Rust não tem GIL: as threads rodam em paralelo, e o compilador impede data races.
  • O Cargo substitui o pip, o venv e companhia; cargo fmt, cargo clippy e cargo test substituem o black, o ruff e o pytest.
  • Dá para misturar as duas linguagens: o PyO3 e o maturin transformam código Rust num módulo Python.

Teste o que aprendeu

1. O que o Rust usa no lugar do None do Python?

2. Em Python, b = a dá dois nomes para a mesma lista. O que let b = a; faz com um Vec no Rust?

3. Qual é o resultado de 7 / 2 em Rust, quando os dois números são inteiros?

4. Você quer acelerar uma função lenta de um projeto Python usando Rust. Quais ferramentas você usa?

Pontuação: 0 / 4

  • The Rust Programming Language (em inglês): o livro oficial. Os capítulos 4 (ownership), 6 (enums e Option), 9 (erros) e 13 (closures e iteradores) são os que mais importam para quem vem do Python.
  • PyO3 user guide (em inglês): tudo sobre chamar Rust a partir do Python (e Python a partir do Rust).
  • Maturin user guide (em inglês): como compilar, testar e publicar pacotes Python feitos com Rust.
  • Rust by Example (em inglês): exemplos curtos que você pode rodar, ótimos se você aprende melhor lendo código.
  • Are We Learning Yet? (em inglês): um panorama dos crates de machine learning e de dados do Rust.