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
Noneque 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
importdela 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.
Conceitos da linguagem: do Python para o Rust
Seção intitulada “Conceitos da linguagem: do Python para o Rust”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 |
Coleções
Seção intitulada “Coleçõ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 |
Ferramentas: do pip para o cargo
Seção intitulada “Ferramentas: do pip para o cargo”É 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.
Web e rede
Seção intitulada “Web e rede”| 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 |
Dados e bancos de dados
Seção intitulada “Dados e bancos de dados”| 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 |
Ciência de dados e machine learning
Seção intitulada “Ciência de dados e machine learning”| 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 |
Blocos do dia a dia
Seção intitulada “Blocos do dia a dia”| 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 |
Lado a lado: uma classe vira struct + impl
Seção intitulada “Lado a lado: uma classe vira struct + impl”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 150Algumas 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.
Lado a lado: duck typing vira traits
Seção intitulada “Lado a lado: duck typing vira traits”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 rodaEm 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: QuackEle diz: Beep boopNo 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.
Lado a lado: None vira Option
Seção intitulada “Lado a lado: None vira Option”Em Python, qualquer valor pode ser None às escondidas, e você só descobre quando usa:
def find_email(user_id): 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 { _ => 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 cadastradodesconhecidoSome("[email protected]")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.
Lado a lado: exceções viram Result e ?
Seção intitulada “Lado a lado: exceções viram Result e ?”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 8080erro: "http" não é um número (invalid digit found in string)erro: a porta 70000 está fora do intervaloA 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: 3cat: 1end: 1O 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.
Lado a lado: o GIL vs threads de verdade
Seção intitulada “Lado a lado: o GIL vs threads de verdade”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 primos25000..50000: 2371 primos50000..75000: 2260 primos75000..100000: 2199 primostotal: 9592O 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):
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathO 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])) # 30print(fastmath.word_count("the quick brown fox")) # 4fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integerO 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.
O que vai fazer você tropeçar vindo do Python
Seção intitulada “O que vai fazer você tropeçar vindo do Python”Todo mundo que vem do Python passa pelos mesmos buracos. Conhecer todos com antecedência ajuda.
Atribuir move o valor
Seção intitulada “Atribuir move o valor”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` trait3 | let mut b = a; | - value moved here4 | 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.
O resto da lista
Seção intitulada “O resto da lista”- Inteiros têm tamanho. O
intdo Python nunca estoura. Umi32do Rust para em 2.147.483.647; passar disso causa um panic num build de debug. Escolhai64ouu64para números grandes, e veja Overflow de inteiros. - Divisão e resto funcionam diferente.
7 / 2dá3em Rust, porque os dois lados são inteiros (o//do Python). Escreva7.0 / 2.0para ter3.5. Além disso,-7 % 3dá-1em Rust, mas2em 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, porque3é um inteiro. Escreva3.0, ou converta comas 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. Useword.chars().next()ouword.chars().nth(i). Veja Strings. Stringvs&str. O Python tem um único tipostr. O Rust tem dois:Stringé dona do texto,&strpega o texto emprestado. Funções normalmente recebem&stre structs normalmente guardamString.- Variáveis são imutáveis por padrão. Escreva
let mutquando 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
ife todomatchprecisam ser completos e ter os tipos certos. Não existe truthiness:if countnão compila, você escreveif count > 0. Ummatchprecisa cobrir todos os casos. - Um HashMap não tem ordem. O
dictdo Python lembra a ordem de inserção; oHashMapdo Rust não. Ordene antes de imprimir, use umBTreeMap, ou adicione o crateindexmap. - 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 checkenquanto edita (ele pula a geração de código e é bem mais rápido), e ocargo run --releasequando 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.
Uma trilha de estudo para desenvolvedores Python
Seção intitulada “Uma trilha de estudo para desenvolvedores Python”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:
- Começando com Rust e Ferramentas do Rust: instale o Rust e rode
cargo new. - Variáveis, Tipos de dados e Overflow de inteiros: os tipos são o primeiro hábito novo.
- Funções, If…Else e Loops: leituras rápidas; repare nas expressões e nos pontos e vírgulas que faltam.
- Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
- Strings:
Stringvs&str, e por que você não consegue indexar uma string. - Structs e Enums, depois Match e Pattern matching.
- Option e Result, depois Erros personalizados.
- Vetores, HashMap e HashSet.
- Closures e Iteradores: as suas comprehensions e generators.
- Traits, Genéricos, Traits comuns e Trait objects.
- Módulos, Cargo e crates e Testes em Rust.
- Lifetimes e Ponteiros inteligentes.
- Concorrência, Estado compartilhado e Async/Await.
- JSON com Serde, e depois construa uma CLI com clap ou uma API web.
Prática
Seção intitulada “Prática”Os dois exercícios trazem um trecho de Python. Converta para Rust.
1. Converta uma comprehension e um dict
Seção intitulada “1. Converta uma comprehension e um dict”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.0Percorrer 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.
2. Converta try/except para Result
Seção intitulada “2. Converta try/except para Result”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 anosignorada "Grace 45": falta o ':'ignorada "Linus: forty": idade inválida: invalid digit found in string1 salvo(s)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,
matche 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,
Optionno lugar deNone, eResult+?no lugar de exceções. - Classes viram uma
struct+impl, duck typing vira traits, e blocoswithviram oDropautomático. UmHashMapnã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 clippyecargo testsubstituem 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- 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.