Python-to-Rust Porting Rules
May 27, 2026 · View on GitHub
Rules for systematically porting Python applications to Rust. Covers principles, patterns, pitfalls, and acceptance criteria. For the step-by-step porting process, see the Python-to-Rust Playbook.
For construct-by-construct lookup (types, I/O, regex, project setup), see the Mapping Reference.
Read first: Porting Principles and Anti-Patterns — non-negotiable principles that override all other guidance.
See also: CLI-Specific Porting Patterns,
Rust General Rules,
Test Coverage for Porting.
For Python rules, see tbd guidelines python-rules.
Core Principles
-
Tests are the specification. The Python test suite defines exactly what the Rust port must do. Byte-for-byte output matching is the goal; any deviation must be explicitly documented, justified, and tracked. (See Strategy Matrix for handling unavoidable differences.)
-
Port behavior, not implementation. Don’t translate Python idioms literally into Rust. Achieve the same behavior using idiomatic Rust patterns.
-
Maintain clear traceability. Every Rust module should have a comment indicating which Python module it corresponds to. Every major function should reference its Python equivalent.
-
Zero-tolerance for test failures. 100% of ported tests must pass. If a difference is unavoidable (e.g., library behavior), document it explicitly and mark the test
#[ignore]with an explanation.
Type and Error Mappings
See Mapping Reference § Types and Mapping Reference § Error Handling for exhaustive mapping tables with code examples.
Porting-specific principles:
- Use
Cow<'_, str>when a function sometimes modifies its input and sometimes returns it unchanged -- avoids allocation in the pass-through case. - Use
Option<T>for PythonNone. Never use sentinel values. HashMapdoes not preserve insertion order (Pythondictdoes since 3.7). UseIndexMapif order matters for behavioral parity.- Critical rule: If the Python function never raises an exception, the Rust function
should NOT return
Result. Match the Python signature exactly.
Module Mapping
Python Modules to Rust Modules
# Python
flowmark/
__init__.py # Package init
formatter/
filling.py # Core formatting
markdown.py # AST manipulation
wrapping/
text_wrapping.py # Text wrapping
sentence.py # Sentence splitting
typography/
quotes.py # Smart quotes
// Rust
src/
lib.rs // Pub API (like __init__.py)
formatter/
mod.rs
filling.rs // Port of filling.py
markdown.rs // Port of markdown.py
wrapping/
mod.rs
text_wrapping.rs // Port of text_wrapping.py
sentence.rs // Port of sentence.py
typography/
mod.rs
quotes.rs // Port of quotes.py
Module Header Convention
Every ported Rust module MUST have a header comment:
//! Port of Python `flowmark/formatter/filling.py`
//!
//! Main formatting pipeline for normalizing Markdown documents.
Function Mapping Convention
Every major ported function MUST reference its Python origin:
/// Port of Python `fill_markdown()` in `filling.py`
///
/// Formats a Markdown document with line wrapping and normalization.
pub fn fill_markdown(text: &str, config: &Config) -> Result<String> {
Dependency Mapping
See Mapping Reference § Dependency Mapping for the full library equivalence table.
Library Evaluation Checklist
Before choosing a Rust crate equivalent:
- Check spec compliance (does it implement the same standards?)
- Check maintenance status (last commit, open issues, maintainer activity)
- Check API compatibility (can it produce the same outputs?)
- Test with real-world inputs from the Python version
- Document any behavioral differences
- Have a backup plan (alternative crate, vendoring, forking)
Porting Sequence
Recommended Order
- Project setup -- Cargo.toml, directory structure, Python submodule
- Core data types -- Config, Error types, shared structs
- Innermost modules first -- Leaf modules with no internal dependencies
- Test as you go -- Port unit tests alongside each module
- Integration modules -- Modules that combine the leaves
- CLI last -- Thin wrapper once library works
- Cross-validation -- Run both implementations against same inputs
Per-Module Workflow
For each Python module:
- Read the Python source (from submodule)
- Create Rust file with module header comment
- Port the tests first (TDD style)
- Implement the functions to make tests pass
- Run
cargo test-- all new tests must pass - Run cross-validation against Python output
Key Pitfalls
1. Regex Anchoring
Rust regex is unanchored by default -- the #1 source of porting bugs.
Add ^ for re.match(), wrap with ^...$ for re.fullmatch(). See
Mapping Reference § Regex for
the full regex porting guide including replacement syntax, flags, and unicode behavior.
2. String Preservation
Don’t “helpfully” modify input:
// WRONG: trims whitespace Python doesn't trim
pub fn process(text: &str) -> String {
text.trim().to_string()
}
// CORRECT: preserves input exactly as Python does
pub fn process(text: &str) -> String {
text.to_string()
}
3. Function Signatures Must Match
def split_frontmatter(text: str) -> tuple[str, str]:
return ("", text) # Returns tuple, never None
// WRONG: different return type
fn split_frontmatter(text: &str) -> Option<(String, String)> { ... }
// CORRECT: matches Python exactly
fn split_frontmatter(text: &str) -> (String, String) {
(String::new(), text.to_string())
}
4. String Indexing
// WRONG: panics on multi-byte characters
let ch = &text[5..6];
// CORRECT: char-boundary safe
let ch = text.chars().nth(5);
// or use char_indices() for slicing
5. Library Type Differences
Library types are version-dependent. Check current docs:
// comrak NodeValue::Text: Vec<u8> (pre-0.4), String (0.4-0.44), Cow<'static, str> (0.45+).
// pulldown-cmark uses CowStr (different API). Always verify against your Cargo.lock version.
6. Integration Test Location
// WRONG: tests/ at workspace root, outside any member crate
my-workspace/tests/test_format.rs
// CORRECT: tests/ inside a specific crate
my-workspace/crates/core/tests/test_format.rs
7. Arena Pattern for AST Libraries
Python AST libraries let you hold references freely. Rust libraries like comrak use arenas that require closure-based APIs:
pub fn with_markdown_ast<F, R>(text: &str, f: F) -> Result<R>
where
F: for<'a> FnOnce(&'a AstNode<'a>) -> Result<R>,
{
let arena = Arena::new();
let root = parse_document(&arena, text, &options);
f(root)
}
8. Edition 2024 Reserved Keywords
Rust Edition 2024 reserves gen as a keyword.
If the Python code uses gen as a variable or function name (common in
generator-related code), you must rename it in Rust:
// WRONG: `gen` is a reserved keyword in Edition 2024
let gen = create_generator();
// CORRECT: rename to avoid keyword conflict
let generator = create_generator();
9. Line Ending Differences
Normalize line endings in tests when they’re not semantically significant. Be explicit about expectations in byte-for-byte comparison tests.
Handling Library Bugs and Differences
Strategy Matrix
| Severity | Impact | Strategy |
|---|---|---|
| Cosmetic | Output differs but is valid | Accept and document |
| Functional | Output incorrect but rare | Workaround with post-processing |
| Critical | Core behavior broken | Vendor/fork the library, or switch libraries |
Workaround Pattern
Mark all workarounds with structured comment prefixes:
HACK:for library workarounds that are intentional and stable.FIXME:for items needing future resolution.
Avoid XXX: — it is not recognized by most linters, IDEs, or CI grep patterns.
Use HACK: for workarounds and FIXME: for known issues instead.
/// WORKAROUND: comrak normalizes list markers to `-`, but Python preserves `*`.
/// This is unfixable without forking comrak's renderer.
/// Impact: minor -- both are valid Markdown.
/// FIXME: This post-processing step should be removed once comrak #567 is merged.
When to Switch Libraries
Switch if you find >3 unfixable differences that affect core behavior. The cost of working around one library’s bugs accumulates rapidly. Research alternatives early.
Post-Port Cleanup
Stale Python-Reference Comments
During porting, agents add comments mapping Rust code back to Python (e.g.,
// Python: self._format_line(text)). After the port stabilizes, many of these become
stale as the Rust code evolves independently.
Schedule a cleanup pass:
- Verify each Python reference comment is still accurate. If the Rust implementation has diverged, update or remove the comment.
- Keep module-level mapping comments
(
//! Ported from Python: flowmark/filling.py) — these remain valuable for traceability. - Remove per-line Python comments that describe implementation details no longer relevant to the Rust code. Rust code should explain itself idiomatically, not as a translation of Python.
- Keep comments that explain non-obvious behavioral parity — e.g., “matches Python’s behavior of returning empty string for None input.”
Visibility Audit
After porting is complete, audit pub visibility.
During porting, agents tend to make everything pub for convenience.
Convert internal-only items to pub(crate). See
Code Review Checklist for details.
Acceptance Criteria
Zero-tolerance completion gate:
- 100% of Python tests pass in Rust
- Byte-for-byte output match on all test fixtures (goal; document deviations with
WORKAROUND:if a library difference makes exact match impossible) - Cross-validation with zero diffs on representative documents (or all diffs explained by documented workarounds)
- All known differences documented with
HACK:orFIXME:comments - All
#[ignore]tests have documented reasons - CLI help text matches Python exactly
- Exit codes match Python behavior
- cargo clippy -- -D warnings passes with zero warnings
- cargo fmt -- --check passes (consistent formatting)
Related Guidelines
- CLI-Specific Porting
- Rust General Rules
- Test Coverage for Porting
- For Python rules, see
tbd guidelines python-rules - For golden testing, see
tbd guidelines golden-testing-guidelines - For TDD methodology, see
tbd guidelines general-tdd-guidelines