Python to Rust Testing with Depyler
Chapter 12: Python to Rust Testing with Depyler
You've built comprehensive Python test suites. But what if you could leverage Rust's safety guarantees and performance? This chapter introduces Depyler, a Python-to-Rust transpiler that preserves program semantics while providing compile-time safety. You'll learn how to migrate Python code to Rust and verify correctness through testing.
What is Depyler?
Depyler transpiles annotated Python code into idiomatic Rust, preserving semantics while providing memory safety and performance improvements.
Key Features:
- Type-directed transpilation: Uses Python type annotations to generate Rust types
- Memory safety analysis: Infers ownership and borrowing patterns
- Semantic verification: Property-based testing verifies behavioral equivalence
- Test preservation: Transpiles both code AND tests
Why Transpile Python to Rust?
- Performance: 10-100x faster execution for compute-intensive code
- Safety: Compile-time memory safety guarantees
- Concurrency: Fearless concurrency with Rust's ownership system
- Deployment: Single binary, no interpreter needed
Install Depyler:
cargo install depylerBasic Transpilation Workflow
Input (fibonacci.py):
Transpile:
depyler transpile fibonacci.pyOutput (fibonacci.rs):
fn fibonacci(n: i32) -> i32 {
if n <= 1 {
return n;
}
fibonacci(n - 1) + fibonacci(n - 2)
}Notice: Type annotations (int) become Rust types (i32), syntax adapts automatically.
Testing Transpiled Code
Depyler doesn't just transpile code—it verifies correctness through semantic equivalence testing.
Python Test (test_fibonacci.py):
import pytest
def test_fibonacci_base_cases():
assert fibonacci(0) == 0
assert fibonacci(1) == 1
def test_fibonacci_recursive():
assert fibonacci(5) == 5
assert fibonacci(10) == 55Transpile with Verification:
depyler transpile fibonacci.py --verifyDepyler generates property-based tests to verify Python and Rust implementations produce identical results:
Running semantic verification...
Testing fibonacci(0)... ✅ Match
Testing fibonacci(1)... ✅ Match
Testing fibonacci(5)... ✅ Match
Testing fibonacci(10)... ✅ Match
Testing fibonacci(-1)... ✅ Match
Semantic Verification: PASSED
100% behavioral equivalence confirmedIf results differ, transpilation fails. This catches subtle semantic bugs.
Type System Mapping
Depyler maps Python types to Rust types conservatively:
Primitive Types:
int→i32(ori64for large values)float→f64bool→boolstr→String
Collections:
list[T]→Vec<T>(owned) or&[T](borrowed)dict[K, V]→HashMap<K, V>set[T]→HashSet<T>tuple[T, U]→(T, U)
Option and Result:
T | None→Option<T>- Functions that raise exceptions →
Result<T, E>
Example:
def find_user(user_id: int) -> dict[str, str] | None:
if user_id < 0:
raise ValueError("Invalid user ID")
if user_id == 0:
return None
return {"id": str(user_id), "name": "User"}Transpiles to:
fn find_user(user_id: i32) -> Result<Option<HashMap<String, String>>, String> {
if user_id < 0 {
return Err("Invalid user ID".to_string());
}
if user_id == 0 {
return Ok(None);
}
let mut user = HashMap::new();
user.insert("id".to_string(), user_id.to_string());
user.insert("name".to_string(), "User".to_string());
Ok(Some(user))
}Exception handling becomes Result<T, E>, None checks become Option<T>.
Migration Complexity Analysis
Before transpiling a large codebase, analyze migration effort:
depyler analyze myproject.pyOutput example:
Migration Complexity Analysis
==============================
File: myproject.py
Lines of Code: 450
Functions: 23
Complexity Score: MEDIUM (65/100)
Supported Features:
✅ Type annotations: 100%
✅ Basic types: 100%
✅ Collections: 95%
✅ Control flow: 100%
✅ Exception handling: 90%
Unsupported Features:
❌ Dynamic evaluation (eval): 2 instances
❌ Reflection (getattr): 1 instance
⚠️ Multiple inheritance: 1 class
Estimated Effort: 2-3 days
Recommendation: Refactor 3 areas before transpilingThis helps prioritize refactoring before migration.
Transpiling Tests
Depyler transpiles pytest tests to Rust tests:
Python (test_math.py):
import pytest
def test_addition():
assert add(2, 3) == 5
def test_division_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)Transpiled Rust (tests/math_test.rs):
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "ZeroDivisionError")]
fn test_division_by_zero() {
divide(10, 0).unwrap();
}
}Run Rust tests:
cargo testAll Python test semantics preserved in Rust.
Ownership and Borrowing Inference
Depyler infers ownership patterns from usage:
Example 1: Ownership Transfer
def process(data: list[int]) -> list[int]:
data.append(42)
return dataRust:
fn process(mut data: Vec<i32>) -> Vec<i32> {
data.push(42);
data
}data is moved (ownership transferred).
Example 2: Borrowing
def sum_list(data: list[int]) -> int:
return sum(data)Rust:
fn sum_list(data: &[i32]) -> i32 {
data.iter().sum()
}data is borrowed (no ownership transfer).
Depyler analyzes usage to choose appropriate ownership semantics.
MCP Integration
Depyler provides MCP tools for AI-powered transpilation workflows:
Setup (~/.config/Claude/claude_desktop_config.json):
{
"mcpServers": {
"depyler": {
"command": "depyler",
"args": ["agent", "start", "--foreground"]
}
}
}Available Tools:
transpile_python- Convert Python to Rustanalyze_migration_complexity- Estimate effortverify_transpilation- Check semantic equivalencepmat_quality_check- Quality analysis
AI assistants can now help with Python-to-Rust migrations.
Incremental Migration Strategy
Don't transpile everything at once. Use incremental migration:
Step 1: Identify Performance Bottlenecks
python -m cProfile myapp.pyFind hot paths (functions consuming most time).
Step 2: Transpile Hot Paths
depyler transpile hot_functions.py --verifyStep 3: Build Rust Library
cargo build --releaseStep 4: Call from Python via PyO3
import mylib_rust
result = mylib_rust.hot_function(data)Step 5: Verify Performance Gains
Benchmark before/after. Expect 10-100x speedup for compute-heavy code.
Step 6: Gradually Migrate More
Repeat for other modules.
Limitations and Workarounds
Not Supported:
eval()/exec()- No dynamic execution in Rust- Monkey patching - Rust doesn't allow runtime modification
- Multiple inheritance - Rust uses traits instead
- Runtime reflection - Limited in Rust
Workarounds:
- Refactor
eval()to static code - Replace monkey patching with explicit functions
- Use traits for multiple inheritance patterns
- Use compile-time reflection (macros)
Depyler warns about unsupported features during analysis.
Testing Strategy for Transpiled Code
- Semantic Verification: Run
--verifyto ensure behavioral equivalence - Property-Based Tests: Use Hypothesis in Python, proptest in Rust
- Benchmark Tests: Verify performance improvements
- Integration Tests: Test Rust/Python interop
- Fuzzing: Use cargo-fuzz to find edge cases
Combine Python testing knowledge with Rust safety guarantees.
Real-World Use Case
Scenario: Data processing pipeline runs slowly in Python.
Original (process.py):
def analyze_data(data: list[dict]) -> dict:
"""Analyze large dataset."""
total = sum(item["value"] for item in data)
avg = total / len(data) if data else 0
return {"total": total, "average": avg}Performance: 2.5 seconds for 1M records.
Transpile:
depyler transpile process.py --verify
cargo build --releasePerformance: 25ms for 1M records. 100x faster.
Tests still pass. Semantics preserved. Zero bugs introduced.
Advanced: Custom Type Mappings
Configure custom type mappings in depyler.toml:
[type_mapping]
"decimal.Decimal" = "rust_decimal::Decimal"
"datetime.datetime" = "chrono::DateTime<Utc>"
"pathlib.Path" = "std::path::PathBuf"This handles third-party types not in standard library.
Combining PMAT and Depyler
Use both tools together for maximum quality assurance during migration:
Step 1: PMAT Quality Check (Python)
# Ensure Python tests are comprehensive
pmat mutate test calculator.py
# Output
Mutation Score: 85%
✅ High-quality tests confirmedStep 2: Depyler Transpilation
# Transpile with semantic verification
depyler transpile calculator.py --verify
# Output
✅ Transpilation successful
✅ Semantic verification passedStep 3: PMAT Quality Check (Rust)
# Run mutation testing on Rust code
pmat mutate test calculator.rs --language rust
# Output
Mutation Score: 87%
✅ Quality maintained after migrationStep 4: Compare Scores
- Python: 85% mutation score
- Rust: 87% mutation score
- Difference: +2% (acceptable)
If Rust score drops significantly, investigate test coverage gaps introduced during transpilation.
Step 5: Performance Benchmark
# Python
time python benchmark.py
# 2.5 seconds
# Rust
time cargo run --release
# 25ms (100x faster)This workflow ensures quality doesn't degrade during migration while delivering massive performance gains.
Common Issues and Solutions:
Issue: Rust mutation score lower than Python
- Cause: Some Python tests didn't transpile correctly
- Solution: Manually review transpiled tests, add missing assertions
Issue: Semantic verification fails
- Cause: Floating-point precision differences
- Solution: Use approximate equality (
pytest.approx→assert_approx)
Issue: Performance not as expected
- Cause: Debug build instead of release
- Solution: Always benchmark with
cargo build --release
Best Practices for Python-Rust Migration
1. Start Small
Don't transpile your entire application. Start with:
- One hot function (identified via profiling)
- Pure functions without side effects
- Functions with clear type annotations
- Code without dynamic features
2. Add Type Annotations First
Before transpiling, ensure comprehensive type coverage:
# Before (untyped)
def process(data):
return [x * 2 for x in data]
# After (typed)
def process(data: list[int]) -> list[int]:
return [x * 2 for x in data]Depyler requires type annotations for accurate transpilation.
3. Test Thoroughly Before Transpiling
Achieve 80%+ code coverage and 75%+ mutation score in Python first. If Python tests are weak, Rust tests will be weak too.
4. Use Semantic Verification Always
Never skip --verify. It's your safety net:
# Always use --verify
depyler transpile module.py --verify
# Don't do this
depyler transpile module.py # ❌ No verification5. Profile Before and After
Measure actual performance gains:
import time
start = time.time()
result = python_function(data)
print(f"Python: {time.time() - start:.3f}s")
start = time.time()
result = rust_function(data)
print(f"Rust: {time.time() - start:.3f}s")Don't assume—measure. Some functions won't benefit from Rust.
6. Keep Python Version as Fallback
During migration, maintain both versions:
try:
from mylib_rust import compute
except ImportError:
from mylib_python import computeThis allows graceful fallback if Rust version has issues.
Course Recommendations
Python to Rust Migration
- Complete transpilation workflows
- Performance optimization techniques
- PyO3 integration patterns
- Enroll at paiml.com
Rust for Python Developers
- Ownership and borrowing explained
- Type system deep dive
- Building fast Python extensions
- Enroll at paiml.com
Advanced Testing with Property-Based Testing
- Hypothesis and proptest mastery
- Semantic equivalence testing
- Cross-language test strategies
- Enroll at paiml.com
Quiz
Final Thoughts
Depyler bridges Python's expressiveness with Rust's safety and performance. By preserving semantics through verification, it enables confident migration of critical code paths. Combined with comprehensive testing (pytest + PMAT + property-based testing), Depyler makes Python-to-Rust migration practical and safe. Use it to optimize hotspots while maintaining the testing rigor you've built throughout this book.
Congratulations on completing Testing in Python! You now have a comprehensive toolkit: unit testing, TDD, mocking, coverage, mutation testing, property-based testing, PMAT quality gates, and Python-to-Rust migration with Depyler. Apply these techniques to build reliable, maintainable, and performant Python applications.
📝 Test Your Knowledge: Python to Rust Testing with Depyler
Take this quiz to reinforce what you've learned in this chapter.