- Python 99.6%
- Nix 0.4%
| .forgejo/workflows | ||
| pydorust | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
pydorust
A Python library bringing Rust programming language paradigms, semantics, and strict typing patterns to Python. Rather than attempting to enforce physical memory safety (which is fundamentally limited by Python's dynamic runtime), pydorust provides familiar Rust semantics, zero-cost abstractions, and structural patterns:
- Macros (
pydorust.macros): Procedural, declarative, and built-in AST metaprogramming. - Traits (
pydorust.trait): True ad-hoc polymorphism and interface decoupling. - Algebraic Data Types (
pydorust.adt): Tagged unions viaEnumwith nestedVariantclasses. - Option and Result (
pydorust.types): Explicit error handling withOption[T](Some,Null) andResult[T, E](Ok,Err). - Interior Mutability (
pydorust.lock):Cell,RefCellwith runtime borrow checking,OnceLock, andLazyLock. - Synchronization Primitives (
pydorust.sync): SemanticMutexandRwLockwith RAII-style context manager guards. - Fixed-Width Numbers (
pydorust.num):u8..u64,i8..i64,usize,isizewith wrapping, checked, saturating, and overflowing arithmetic. - Panics and Unsafe Contracts (
pydorust.panics,pydorust.unsafe,pydorust.mem): Explicitpanic(),unreachable(),@unsafe_func, and raw memory allocations.
Installation
pip install pydorust
Macros
The macro system brings syntactic metaprogramming to Python, inspired by Rust's procedural and declarative macros. Macros operate at the Python Abstract Syntax Tree (AST) level, allowing you to transform syntax, inline computations, and generate boilerplate at compile time or import time.
Invocation Forms
pydorust supports three ways to invoke macros in code:
m_call(macro, target, *args): Invokes a macro as an expression or statement. The first argument is the macro, and subsequent arguments are passed as AST nodes.@m_decorator(macro, *args): Wraps a macro for use as a standard Python decorator on functions or classes.m_derive(macro, *args): A statement-level macro that intercepts and transforms the next AST node immediately following the call.
Built-in Macros
pydorust.macros.builtins provides standard macros:
Compile-Time Evaluation: comptime
Evaluates Python expressions at compile/expansion time and inlines the literal result into the AST:
from pydorust.macros import m_call
from pydorust.macros.builtins import comptime
# Evaluated once during macro expansion:
BUFFER_SIZE = m_call(comptime, 1024 * 1024 * 4)
# Compiles to: BUFFER_SIZE = 4194304
Compile-Time Environment: env and option_env
Reads environment variables during macro expansion and inlines their string literals into the AST (identical to Rust's env!() / option_env!()):
from pydorust.macros import m_call
from pydorust.macros.builtins import env
# Inlined as a string constant at compile time:
BUILD_PROFILE = m_call(env, "APP_ENV")
Compile-Time File Inclusion: include_str and include_bytes
Embeds the contents of an external file directly into the AST as a string or bytes literal at compile time (mirroring Rust's include_str!() / include_bytes!()):
from pydorust.macros import m_call
from pydorust.macros.builtins import include_str
# Inlines file contents directly into the code:
SCHEMA_JSON = m_call(include_str, "schema.json")
Declarative Macros: @m_define_declarative
Inspired by Rust's macro_rules!, declarative macros allow writing template-based code generators. Arguments represent AST variables, and names or strings prefixed with __q_ are replaced with the input AST nodes:
from typing import Any
from pydorust.macros import m_call, m_decorator
from pydorust.macros.builtins import m_define_declarative
@m_decorator(m_define_declarative)
def make_getter(field_name: Any) -> Any:
def __q_field_name(self):
return self._value
class Item:
def __init__(self, value: int) -> None:
self._value = value
# Generates: def get_value(self): return self._value
m_call(make_getter, get_value)
item = Item(42)
print(item.get_value()) # 42
Declarative macros also support substituting into classes, docstrings, and f-strings:
@m_decorator(m_define_declarative)
def create_service(service_name: Any, endpoint: Any) -> Any:
class __q_service_name:
"""Client for __q_service_name."""
URL = __q_endpoint
def connect(self) -> str:
return f"Connected to {__q_service_name.URL}"
m_call(create_service, PaymentService, "https://api.payments.com")
Imperative AST Macros: @m_define
For advanced transformations, @m_define allows writing procedural macros that inspect and manipulate AST nodes directly using MacroContext:
import ast
from pydorust.macros import m_define, MacroContext, MacroResult
@m_define
def assert_positive(ctx: MacroContext, node: ast.AST, *args: ast.AST) -> MacroResult:
code = ctx.unparse(node)
return ctx.parse(
f"if not ({code} > 0): raise ValueError(f'{code} must be positive, got {{{code}}}')",
unwrap_module=True,
)
Tooling and Build Integration
Macros can be expanded in multiple workflows:
-
CLI Tool (
python -m pydorust.macros):# Expand a file and print the transformed Python code python -m pydorust.macros expand my_module.py # Compile a directory, expanding all macros into a target build folder python -m pydorust.macros compile src/ --out build/ # Generate type stub (.pyi) files for macro-expanded code python -m pydorust.macros stubs src/ --out stubs/ -
Hatchling Build Hook:
pydorustregisters ahatchbuild hook (pydorust.plugins.hatch). When building wheels or sdist packages (hatch build), macros are automatically expanded ahead of packaging. -
Pytest Integration:
pydorustregisters a pytest plugin (pydorust.plugins.pytest) viapytest11. Any test suite running under pytest automatically expands macros at import time without extra configuration. -
Runtime Expansion:
from pydorust.macros import enable_runtime_expand, disable_runtime_expand enable_runtime_expand() # Installs import hook in sys.meta_path import my_macro_code # Expanded on import disable_runtime_expand()
Traits
pydorust introduces Rust-style traits, decoupling method and property declarations from class inheritance. This enables ad-hoc polymorphism (extension methods) on existing classes — including built-in types like int, str, or third-party classes — without monkey-patching.
Core Components
Trait: Base class for defining a trait interface.@trait_method: Declares a required method that implementors must provide.@default_method: Provides a default implementation that can call other trait methods.@trait_property: Declares a required property.@default_property: Provides a default property implementation.@impl(Trait, for_type=...): Registers a trait implementation for a specific type and verifies completeness at registration time.
Example
from pydorust.trait import (
Trait,
impl,
trait_method,
default_method,
trait_property,
default_property,
)
class Summary(Trait):
@trait_method
def summarize_author(self) -> str:
"""Required method: implementors must provide this."""
@default_method
def summarize(self) -> str:
"""Provided default method: calls summarize_author()."""
return f"(Read more from {self.summarize_author()}...)"
@trait_property
def headline(self) -> str:
"""Required property."""
@default_property
def full_summary(self) -> str:
"""Provided default property combining headline and summarize."""
return f"{self.headline} -- {self.summarize()}"
class Article:
def __init__(self, title: str, author: str) -> None:
self.title = title
self.author = author
@impl(Summary)
class ArticleSummary(Article):
def summarize_author(self) -> str:
return f"@{self.author}"
@property
def headline(self) -> str:
return self.title
article = Article("Rust in Python", "rus07tam")
# Type checking works seamlessly:
assert isinstance(article, Summary)
# Dispatch style A: Trait proxy wrapper
summary_view = Summary(article)
print(summary_view.summarize_author()) # "@rus07tam"
print(summary_view.summarize()) # "(Read more from @rus07tam...)"
print(summary_view.full_summary) # "Rust in Python -- (Read more from @rus07tam...)"
# Dispatch style B: Static / functional call
print(Summary.summarize(article)) # "(Read more from @rus07tam...)"
print(
Summary.full_summary(article)
) # "Rust in Python -- (Read more from @rus07tam...)"
Algebraic Data Types
In Rust, algebraic data types are represented via enum, where each variant can optionally contain different types and amounts of data. In pydorust, this pattern is implemented through the Enum class containing nested Variant classes.
Each nested Variant class is automatically treated as an immutable (frozen) data structure:
from pydorust.adt import Enum, Variant
class Message(Enum):
# Unit variant (no data)
class Quit(Variant):
pass
# Struct-like variant with fields
class Move(Variant):
x: int
y: int
class Write(Variant):
text: str
class ChangeColor(Variant):
r: int
g: int
b: int
Instantiation and Type Guarantees
Variants can be instantiated directly as members of the Enum:
msg1 = Message.Quit
msg2 = Message.Move(x=10, y=20)
msg3 = Message.Write(text="Hello, Rust!")
# Type hierarchy is strictly verified:
assert isinstance(msg1, Message)
assert isinstance(msg2, Message)
assert isinstance(msg2, Message.Move)
assert issubclass(Message.Move, Message)
Handling Variants
Variants can be inspected and branched on using standard isinstance checks:
def process_message(msg: Message) -> str:
if isinstance(msg, Message.Quit):
return "Application quit."
if isinstance(msg, Message.Move):
return f"Moved to coordinates ({msg.x}, {msg.y})."
if isinstance(msg, Message.Write):
return f"Message content: {msg.text}"
if isinstance(msg, Message.ChangeColor):
return f"Color changed to RGB({msg.r}, {msg.g}, {msg.b})."
raise TypeError(f"Unknown message variant: {msg}")
print(process_message(Message.Move(x=100, y=200)))
# "Moved to coordinates (100, 200)."
Types: Option and Result
Rust eliminates null reference errors via Option<T> and manages fallible operations through Result<T, E>.
Option[T] (Some and Null)
Represents an optional value: every Option is either Some(value) or Null:
from pydorust.types import Option, Some, Null, ret_option
@ret_option
def find_first_even(numbers: list[int]) -> int | None:
for n in numbers:
if n % 2 == 0:
return n
return None
opt: Option[int] = find_first_even([1, 3, 5, 8, 9])
assert opt.is_some()
assert opt.unwrap() == 8
empty_opt: Option[int] = find_first_even([1, 3, 5])
assert empty_opt.is_null()
assert empty_opt.unwrap_or(0) == 0
# Functional combinators: .map(), .filter(), .and_then()
doubled = opt.map(lambda x: x * 2).unwrap_or(0) # 16
Result[T, E] (Ok and Err)
Represents either success (Ok) or failure (Err):
from pydorust.types import Result, Ok, Err, ret_result
@ret_result
def parse_int(value: str) -> int:
return int(value)
res_ok: Result[int, Exception] = parse_int("42")
assert res_ok.is_ok()
assert res_ok.unwrap() == 42
res_err: Result[int, Exception] = parse_int("invalid")
assert res_err.is_err()
assert res_err.unwrap_or(0) == 0
# Chaining operations safely:
computed = (
parse_int("10")
.map(lambda x: x * 5)
.and_then(lambda x: Ok(f"Result: {x}"))
.unwrap_or("Fallback")
)
print(computed) # "Result: 50"
Interior Mutability
The pydorust.lock module brings Rust-style interior mutability primitives to Python:
RefCell[T]: Runtime Borrow Checking
RefCell provides dynamically checked borrow rules at runtime using context managers:
- Multiple immutable borrows (
with cell.borrow():) are allowed simultaneously. - Only one mutable borrow (
with cell.borrow_mut():) is permitted at a time. - Conflicting borrows raise a
RuntimeError:
from pydorust.lock import RefCell
data = RefCell([1, 2, 3])
# Multiple immutable borrows:
with data.borrow() as b1, data.borrow() as b2:
print(b1.value, b2.value)
# Single mutable borrow:
with data.borrow_mut() as m:
m.value.append(4)
# Conflicting borrows raise RuntimeError:
with data.borrow():
with data.borrow_mut(): # Raises RuntimeError: "already borrowed"
pass
Cell[T]
A container providing interior mutability for copyable or value-like data without borrow-check tracking overhead:
from pydorust.lock import Cell
cell = Cell(10)
print(cell.unsafe_get()) # 10
cell.unsafe_set(25)
print(cell.unsafe_get()) # 25
OnceLock[T] and LazyLock[T]
Thread-safe primitives for once-initialization and lazy evaluation:
from pydorust.lock import OnceLock, LazyLock
# OnceLock: Write once, read many times
config = OnceLock()
assert config.try_get().is_null()
config.get_or_init(lambda: {"env": "production", "port": 8080})
print(config.unwrap()) # {'env': 'production', 'port': 8080}
# LazyLock: Computed on first access
heavy_resource = LazyLock(lambda: sum(range(1_000_000)))
print(heavy_resource.get()) # Computed and cached on first call
Synchronization Primitives
Provides RAII-style semantic synchronization primitives mirroring Rust's std::sync:
Mutex[T] and MutexGuard
Protects shared data behind an exclusive lock:
import threading
from pydorust.sync import Mutex
counter = Mutex(0)
def worker():
for _ in range(1000):
with counter.lock() as guard:
guard.value += 1
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
with counter.lock() as guard:
print(guard.value) # 5000
RwLock[T]
Allows concurrent read access by multiple readers, or exclusive access by a single writer:
from pydorust.sync import RwLock
data = RwLock({"key": "initial"})
# Readers
with data.read() as reader:
print(reader.value["key"])
# Writer
with data.write() as writer:
writer.value["key"] = "updated"
Fixed-Width Numbers
pydorust.num introduces fixed-width numeric types matching Rust integer specifications:
- Unsigned:
u8,u16,u32,u64,usize - Signed:
i8,i16,i32,i64,isize
Fixed-width types prevent silent overflows and provide explicit Rust arithmetic modes:
from pydorust.num import u8, i32
# Bounds checking:
val = u8(255)
# u8(256) raises Panic: "out of bounds"
# 1. Wrapping arithmetic (modulo 2^N)
assert u8(255).wrapping_add(2) == 1
assert u8(0).wrapping_sub(1) == 255
# 2. Checked arithmetic (returns Option[T] to prevent panics)
assert u8(250).checked_add(5).unwrap() == 255
assert u8(250).checked_add(10).is_null()
# 3. Saturating arithmetic (clamps to MIN / MAX bounds)
assert u8(250).saturating_add(20) == 255
assert u8(10).saturating_sub(20) == 0
# 4. Overflowing arithmetic (returns tuple: (result, did_overflow))
res, overflow = u8(250).overflowing_add(10)
assert res == 4 and overflow is True
Panics, Unsafe, and Memory Management
Unrecoverable Errors (pydorust.panics)
For irrecoverable conditions, pydorust provides explicit panic primitives:
panic("reason"): Terminates the program or raisesPanic(depending onpanic_mode).unimplemented(): Explicit crash for missing features.unreachable(): Marks unreachable code paths.panic_mode(PanicMode.ABORT | PanicMode.RAISE): Configures whether panics terminate the process (os._exit(1)) or raise a PythonPanicexception (useful for test runners).
from pydorust.panics import panic, unimplemented, unreachable, panic_mode, PanicMode
panic_mode(PanicMode.RAISE)
def handle_choice(option: int):
if option == 1:
return "Option 1"
elif option == 2:
return "Option 2"
else:
unreachable()
Unsafe Contracts (pydorust.unsafe)
In Rust, operations that circumvent safety invariants must reside inside unsafe blocks. pydorust establishes this explicit contract using @unsafe_func and unsafe_call:
from pydorust.unsafe import unsafe_func, unsafe_call
@unsafe_func
def raw_hardware_access(address: int) -> None:
print(f"Accessing memory at {address:#x}")
# raw_hardware_access(0x1000) # Type error / linter warning
unsafe_call(raw_hardware_access, 0x1000) # Explicit contract
Low-Level Memory (pydorust.mem)
Exposes direct memory management primitives wrapping ctypes.pythonapi:
from pydorust.mem import RawBuffer
with RawBuffer(128) as buf:
buf.write(b"Hello from raw memory!")
data = buf.read(22)
print(data) # b'Hello from raw memory!'
License
This project is dedicated to the public domain under the Unlicense. See LICENSE for details.