Introdução
Python é uma das linguagens mais populares do mundo, adorada por sua simplicidade e ecossistema rico. Porém, quando performance e uso de memória se tornam críticos — processamento de dados em larga escala, computação científica, serviços de baixa latência — os limites do Python ficam evidentes. Rust oferece performance de linguagem de sistemas com garantias de segurança de memória, tornando-a a escolha ideal para complementar ou substituir código Python em hot paths.
Este artigo é um guia prático para desenvolvedores Python que querem adotar Rust, seja migrando código existente ou criando extensões Python escritas em Rust. Vamos cobrir desde o mapeamento mental entre as duas linguagens até a integração direta via PyO3.
O Problema: Limites de Performance do Python
Python — Código Típico que Precisa de Performance
# Python: Processamento de dados que demora minutos
import json
from typing import List, Dict
def processar_logs(arquivo: str) -> Dict[str, int]:
"""Conta ocorrências de cada status code em um arquivo de log JSON."""
contagem: Dict[str, int] = {}
with open(arquivo, 'r') as f:
for linha in f:
try:
log = json.loads(linha)
status = str(log.get('status', 'desconhecido'))
contagem[status] = contagem.get(status, 0) + 1
except json.JSONDecodeError:
continue
return contagem
# Com 10 milhões de linhas: ~45 segundos em Python
resultado = processar_logs("access.log")
print(resultado)
Rust — Mesmo Algoritmo, Muito Mais Rápido
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use serde::Deserialize;
#[derive(Deserialize)]
struct LogEntry {
#[serde(default = "status_padrao")]
status: u16,
}
fn status_padrao() -> u16 {
0
}
fn processar_logs(arquivo: &str) -> io::Result<HashMap<u16, u64>> {
let file = File::open(arquivo)?;
let reader = BufReader::with_capacity(64 * 1024, file);
let mut contagem: HashMap<u16, u64> = HashMap::new();
for linha in reader.lines() {
let linha = linha?;
if let Ok(log) = serde_json::from_str::<LogEntry>(&linha) {
*contagem.entry(log.status).or_insert(0) += 1;
}
}
Ok(contagem)
}
fn main() -> io::Result<()> {
// Com 10 milhões de linhas: ~3 segundos em Rust
let resultado = processar_logs("access.log")?;
for (status, count) in &resultado {
println!("{status}: {count}");
}
Ok(())
}
Dependências do exemplo Rust:
# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Mapeamento de Conceitos: Python vs Rust
Tipos Básicos
| Python | Rust | Notas |
|---|---|---|
int | i32, i64, u32, u64 | Rust tem tamanho fixo |
float | f32, f64 | |
str | String ou &str | String é owned, &str é referência |
bool | bool | |
None | Option::None | |
list | Vec<T> | |
dict | HashMap<K, V> | |
tuple | (T1, T2, ...) | |
set | HashSet<T> | |
bytes | Vec<u8> ou &[u8] |
Estruturas de Dados
Python: Classes
from dataclasses import dataclass
from typing import Optional
@dataclass
class Usuario:
id: int
nome: str
email: str
idade: Optional[int] = None
def saudacao(self) -> str:
return f"Olá, {self.nome}!"
def is_adulto(self) -> bool:
if self.idade is None:
return False
return self.idade >= 18
Rust: Structs com impl
#[derive(Debug, Clone)]
pub struct Usuario {
pub id: u64,
pub nome: String,
pub email: String,
pub idade: Option<u8>,
}
impl Usuario {
pub fn new(id: u64, nome: String, email: String) -> Self {
Usuario { id, nome, email, idade: None }
}
pub fn saudacao(&self) -> String {
format!("Olá, {}!", self.nome)
}
pub fn is_adulto(&self) -> bool {
self.idade.map_or(false, |i| i >= 18)
}
}
Tratamento de Erros
Python: try/except
def ler_numero(caminho: str) -> int:
try:
with open(caminho, 'r') as f:
conteudo = f.read().strip()
return int(conteudo)
except FileNotFoundError:
print(f"Arquivo {caminho} não encontrado")
return 0
except ValueError:
print(f"Conteúdo de {caminho} não é um número")
return 0
Rust: Result e pattern matching
use std::fs;
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum LeituraError {
#[error("Arquivo não encontrado: {caminho}")]
ArquivoNaoEncontrado {
caminho: String,
#[source]
fonte: std::io::Error,
},
#[error("Conteúdo não é um número válido")]
ParseError(#[from] ParseIntError),
}
fn ler_numero(caminho: &str) -> Result<i64, LeituraError> {
let conteudo = fs::read_to_string(caminho)
.map_err(|e| LeituraError::ArquivoNaoEncontrado {
caminho: caminho.to_string(),
fonte: e,
})?;
let numero: i64 = conteudo.trim().parse()?;
Ok(numero)
}
fn main() {
match ler_numero("numero.txt") {
Ok(n) => println!("Número lido: {n}"),
Err(e) => eprintln!("Erro: {e}"),
}
}
# Cargo.toml
[dependencies]
thiserror = "2"
Iteradores e Compreensões
Python: List comprehension
# Python
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# List comprehension
pares_dobrados = [n * 2 for n in numeros if n % 2 == 0]
# [4, 8, 12, 16, 20]
# Generator expression
soma = sum(n * 2 for n in numeros if n % 2 == 0)
Rust: Iterator chain
fn main() {
let numeros = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Equivalente à list comprehension
let pares_dobrados: Vec<i32> = numeros
.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * 2)
.collect();
// [4, 8, 12, 16, 20]
// Equivalente ao generator expression com sum
let soma: i32 = numeros
.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * 2)
.sum();
println!("Pares dobrados: {:?}", pares_dobrados);
println!("Soma: {soma}");
}
Migração Gradual com PyO3 e Maturin
A estratégia mais eficiente é migrar gradualmente, reescrevendo apenas os hot paths em Rust e mantendo o restante em Python.
Passo 1: Configurar o Projeto
# Instalar maturin
pip install maturin
# Criar projeto Rust como extensão Python
maturin init --bindings pyo3
# Cargo.toml
[package]
name = "minha_lib_rust"
version = "0.1.0"
edition = "2024"
[lib]
name = "minha_lib_rust"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.23", features = ["extension-module"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Passo 2: Escrever a Função Rust
// src/lib.rs
use pyo3::prelude::*;
use std::collections::HashMap;
/// Processa um arquivo de log e retorna contagem de status codes.
/// Esta função é chamada a partir do Python.
#[pyfunction]
fn processar_logs(arquivo: &str) -> PyResult<HashMap<u16, u64>> {
let conteudo = std::fs::read_to_string(arquivo)
.map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;
let mut contagem: HashMap<u16, u64> = HashMap::new();
for linha in conteudo.lines() {
if let Ok(valor) = serde_json::from_str::<serde_json::Value>(linha) {
if let Some(status) = valor.get("status").and_then(|s| s.as_u64()) {
*contagem.entry(status as u16).or_insert(0) += 1;
}
}
}
Ok(contagem)
}
/// Calcula a média de uma lista de números.
#[pyfunction]
fn media(numeros: Vec<f64>) -> PyResult<f64> {
if numeros.is_empty() {
return Err(pyo3::exceptions::PyValueError::new_err(
"Lista não pode estar vazia",
));
}
Ok(numeros.iter().sum::<f64>() / numeros.len() as f64)
}
/// Classe Rust exposta ao Python.
#[pyclass]
struct Contador {
valores: HashMap<String, u64>,
}
#[pymethods]
impl Contador {
#[new]
fn new() -> Self {
Contador { valores: HashMap::new() }
}
fn incrementar(&mut self, chave: &str) {
*self.valores.entry(chave.to_string()).or_insert(0) += 1;
}
fn obter(&self, chave: &str) -> u64 {
*self.valores.get(chave).unwrap_or(&0)
}
fn top_n(&self, n: usize) -> Vec<(String, u64)> {
let mut items: Vec<_> = self.valores.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
items.sort_by(|a, b| b.1.cmp(&a.1));
items.truncate(n);
items
}
}
/// Módulo Python
#[pymodule]
fn minha_lib_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(processar_logs, m)?)?;
m.add_function(wrap_pyfunction!(media, m)?)?;
m.add_class::<Contador>()?;
Ok(())
}
Passo 3: Compilar e Usar no Python
# Compilar em modo desenvolvimento (rápido)
maturin develop --release
# Ou instalar como pacote
maturin build --release
pip install target/wheels/minha_lib_rust-*.whl
# app.py — usando a extensão Rust no Python
import minha_lib_rust
# Funções Rust chamadas naturalmente do Python
resultado = minha_lib_rust.processar_logs("access.log")
print(f"Status codes: {resultado}")
media = minha_lib_rust.media([1.5, 2.5, 3.5, 4.5])
print(f"Média: {media}")
# Classe Rust usada como classe Python
contador = minha_lib_rust.Contador()
for palavra in ["rust", "python", "rust", "rust", "python", "go"]:
contador.incrementar(palavra)
print(f"Rust: {contador.obter('rust')}")
print(f"Top 2: {contador.top_n(2)}")
Passo 4: Benchmark para Validar Ganho
# benchmark.py
import time
import json
import minha_lib_rust
def processar_python(arquivo: str) -> dict:
contagem = {}
with open(arquivo, 'r') as f:
for linha in f:
try:
log = json.loads(linha)
status = str(log.get('status', 0))
contagem[status] = contagem.get(status, 0) + 1
except json.JSONDecodeError:
continue
return contagem
# Benchmark Python
inicio = time.time()
resultado_py = processar_python("access.log")
tempo_py = time.time() - inicio
print(f"Python: {tempo_py:.3f}s")
# Benchmark Rust
inicio = time.time()
resultado_rs = minha_lib_rust.processar_logs("access.log")
tempo_rs = time.time() - inicio
print(f"Rust: {tempo_rs:.3f}s")
print(f"Speedup: {tempo_py / tempo_rs:.1f}x")
Resultado típico: 10-50x mais rápido para tarefas CPU-bound.
Armadilhas Comuns para Pythonistas
1. Ownership — O Conceito Mais Difícil
# Python: tudo é referência, cópia é explícita
lista = [1, 2, 3]
outra = lista # Ambas apontam para a mesma lista
outra.append(4)
print(lista) # [1, 2, 3, 4] — modificou a original!
fn main() {
let lista = vec![1, 2, 3];
let outra = lista; // MOVE — lista não é mais válida
// println!("{:?}", lista); // ERRO: valor foi movido
println!("{:?}", outra); // OK: outra é a dona agora
// Para compartilhar, use referências:
let lista = vec![1, 2, 3];
let referencia = &lista; // Empréstimo — lista continua válida
println!("{:?}", lista);
println!("{:?}", referencia);
}
2. Strings em Rust São Mais Complexas
# Python: um tipo de string (str), simples e direto
nome = "Maria"
nome_upper = nome.upper()
print(f"Olá, {nome_upper}")
fn main() {
// Rust tem DOIS tipos principais de string:
let nome: &str = "Maria"; // &str: fatia de string, emprestada
let nome_owned: String = "Maria".to_string(); // String: owned, no heap
// Funções geralmente aceitam &str (mais flexível)
fn saudacao(nome: &str) -> String {
format!("Olá, {}", nome.to_uppercase())
}
println!("{}", saudacao(nome)); // &str direto
println!("{}", saudacao(&nome_owned)); // &String → &str automaticamente
}
3. Não Existe None Direto — Use Option
# Python
def buscar_usuario(id: int) -> dict | None:
if id == 42:
return {"nome": "Maria"}
return None
usuario = buscar_usuario(42)
if usuario is not None:
print(usuario["nome"])
use std::collections::HashMap;
fn buscar_usuario(id: u64) -> Option<HashMap<String, String>> {
if id == 42 {
let mut user = HashMap::new();
user.insert("nome".to_string(), "Maria".to_string());
Some(user)
} else {
None
}
}
fn main() {
// Pattern matching (idiomático)
if let Some(usuario) = buscar_usuario(42) {
println!("{}", usuario["nome"]);
}
// Ou com map/unwrap_or
let nome = buscar_usuario(42)
.and_then(|u| u.get("nome").cloned())
.unwrap_or_else(|| "Desconhecido".to_string());
println!("{nome}");
}
4. Sem Herança — Use Composição e Traits
# Python: Herança
class Animal:
def __init__(self, nome: str):
self.nome = nome
def falar(self) -> str:
raise NotImplementedError
class Cachorro(Animal):
def falar(self) -> str:
return f"{self.nome} diz: Au au!"
class Gato(Animal):
def falar(self) -> str:
return f"{self.nome} diz: Miau!"
// Rust: Traits (sem herança)
trait Animal {
fn nome(&self) -> &str;
fn falar(&self) -> String;
}
struct Cachorro {
nome: String,
}
impl Animal for Cachorro {
fn nome(&self) -> &str {
&self.nome
}
fn falar(&self) -> String {
format!("{} diz: Au au!", self.nome)
}
}
struct Gato {
nome: String,
}
impl Animal for Gato {
fn nome(&self) -> &str {
&self.nome
}
fn falar(&self) -> String {
format!("{} diz: Miau!", self.nome)
}
}
fn apresentar(animal: &dyn Animal) {
println!("{}", animal.falar());
}
fn main() {
let rex = Cachorro { nome: "Rex".into() };
let mimi = Gato { nome: "Mimi".into() };
apresentar(&rex);
apresentar(&mimi);
}
5. Async/Await: Conceito Similar, Execução Diferente
# Python: asyncio
import asyncio
import aiohttp
async def buscar_url(url: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
async def main():
resultado = await buscar_url("https://httpbin.org/get")
print(resultado[:100])
asyncio.run(main())
// Rust: tokio + reqwest
use anyhow::Result;
async fn buscar_url(url: &str) -> Result<String> {
let resp = reqwest::get(url).await?;
let texto = resp.text().await?;
Ok(texto)
}
#[tokio::main]
async fn main() -> Result<()> {
let resultado = buscar_url("https://httpbin.org/get").await?;
println!("{}", &resultado[..100.min(resultado.len())]);
Ok(())
}
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
anyhow = "1"
Estratégia de Migração Recomendada
- Identifique hot paths — Use profiling (cProfile, py-spy) para encontrar os gargalos
- Isole a interface — Defina tipos claros de entrada/saída
- Reescreva em Rust com PyO3 — Mantenha a mesma API
- Benchmark — Confirme o ganho de performance
- Itere — Migre mais partes conforme necessário
| Fase | Python | Rust |
|---|---|---|
| Fase 1 | Tudo | Nada |
| Fase 2 | Orquestração, I/O | Hot paths (CPU-bound) |
| Fase 3 | Scripts, glue code | Core do sistema |
| Fase 4 | Opcional | Tudo |
Checklist de Migração
- Profile primeiro — Não adivinhe, meça
- PyO3 + maturin — Integração Python-Rust sem dor
- Comece pelos hot paths — Maior impacto, menor risco
- Mantenha a API Python — Migração transparente
- Teste em ambos — pytest e cargo test
- Benchmark comparativo — Valide o ganho real
- Ownership é seu amigo — Abrace, não lute contra
- Use
ResulteOption— Nuncaunwrap()em produção - Iteradores, não loops — Mais idiomático e performático
- Documente a interface — PyO3 gera docstrings automaticamente
Veja Também
- Tutorial: Primeiros Passos com Rust — Fundamentos da linguagem para iniciantes
- Receita: Parse de JSON — Serde para processar JSON como em Python
- Otimização de Performance — Extraia o máximo de performance
- Boas Práticas de Error Handling — Result e Option para Pythonistas
- Padrões de Projeto em Rust — Alternativas a herança e classes
- Migração de Node.js para Rust — Guia similar para desenvolvedores Node.js