Migrar de Python para Rust: Guia 2026 | Rust Brasil

Guia de migração de Python para Rust com PyO3: FFI, bindings, performance e estratégia incremental.

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

PythonRustNotas
inti32, i64, u32, u64Rust tem tamanho fixo
floatf32, f64
strString ou &strString é owned, &str é referência
boolbool
NoneOption::None
listVec<T>
dictHashMap<K, V>
tuple(T1, T2, ...)
setHashSet<T>
bytesVec<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

  1. Identifique hot paths — Use profiling (cProfile, py-spy) para encontrar os gargalos
  2. Isole a interface — Defina tipos claros de entrada/saída
  3. Reescreva em Rust com PyO3 — Mantenha a mesma API
  4. Benchmark — Confirme o ganho de performance
  5. Itere — Migre mais partes conforme necessário
FasePythonRust
Fase 1TudoNada
Fase 2Orquestração, I/OHot paths (CPU-bound)
Fase 3Scripts, glue codeCore do sistema
Fase 4OpcionalTudo

Checklist de Migração

  1. Profile primeiro — Não adivinhe, meça
  2. PyO3 + maturin — Integração Python-Rust sem dor
  3. Comece pelos hot paths — Maior impacto, menor risco
  4. Mantenha a API Python — Migração transparente
  5. Teste em ambos — pytest e cargo test
  6. Benchmark comparativo — Valide o ganho real
  7. Ownership é seu amigo — Abrace, não lute contra
  8. Use Result e Option — Nunca unwrap() em produção
  9. Iteradores, não loops — Mais idiomático e performático
  10. Documente a interface — PyO3 gera docstrings automaticamente

Veja Também