with one click
rust
// Strict set of rules in terms of codebase development, design patterns, and best practices. Use when the user wants to develop a new feature or refactor existing code.
// Strict set of rules in terms of codebase development, design patterns, and best practices. Use when the user wants to develop a new feature or refactor existing code.
Use when debugging event loop hangs, task scheduling issues, call_soon vs call_soon_threadsafe confusion, Future callback timing, _enter_task/_leave_task conflicts, GIL contention patterns, per-step vs per-drive task context, uvloop compatibility problems, sniffio/anyio backend detection failures, streaming backpressure deadlocks, or native runtime context issues on the asyncio thread. Also use when verifying asyncio assumptions via quick Python one-liners.
Quick reference for building full-stack Databricks Apps with apx (React + FastAPI). Use when working on apx projects, creating routes, adding components, or managing dev servers.
Create git commits with emoji prefixes and conventional commit messages. Use when the user wants to commit changes, push code, or asks to use "just pm" command.
| name | rust |
| description | Strict set of rules in terms of codebase development, design patterns, and best practices. Use when the user wants to develop a new feature or refactor existing code. |
Priority: Correctness > Safety > Readability > Performance
Adapted from The Power of Ten (Holzmann) for Rust.
for x in collection) over manual while i < len with index arithmetic.&str over String, slices over Vec, borrows over clones. Allocate only when you must own.debug_assert! for invariants within functions. Validate inputs at public API boundaries with Result/Option, not panics.pub only when required.Result. Use ? propagation or explicit handling. Annotate intentional ignores with let _ =.// bad — raw index arithmetic repeated everywhere
let mut i = 0;
while i < bytes.len() {
if bytes[i] == target { return i; }
i += 1;
}
// good — domain cursor encapsulates iteration
let mut scanner = Scanner::new(bytes);
while let Some(b) = scanner.peek() {
if b == target { return scanner.position(); }
scanner.advance();
}
#[deny(warnings)] cleanly. Run cargo clippy and address all lints.#[expect(lint, reason = "...")] instead of #[allow]. #[expect] warns when the suppression becomes unnecessary, preventing stale silencing. Always include a reason string. (M-LINT-OVERRIDE-EXPECT)panic!, unwrap(), or unreachable!() for recoverable errors — use Result. expect() is acceptable only for proven invariants with a descriptive message. (M-PANIC-IS-STOP)unsafe only when required by 3rd-party libraries (e.g. PyO3 macros). No other reasons to write unsafe code.Sources: Rust API Guidelines C-CONV, C-GETTER; Microsoft M-CONCISE-NAMES
as_ — cheap reference-to-reference (no allocation, no copy)to_ — expensive conversion, may allocate (e.g. to_string())into_ — consumes self, returns owned valueget_ prefix on getters. Use fn name(&self) -> &str, not fn get_name().From<T>, never Into<T>. The blanket impl gives you Into for free.Service, Manager, Factory, Handler, Processor. If the name needs a suffix, the type does too much.const with a doc comment. No bare numbers, bytes, or strings in logic.// bad
if b == b'\\' { i += 2; }
// good
const ESCAPE_BYTE: u8 = b'\\';
if b == ESCAPE_BYTE { scanner.skip_escaped(); }
Sources: Microsoft M-APP-ERROR, M-ERRORS-CANONICAL-STRUCTS
thiserror for library crates, anyhow/eyre for application crates. Libraries expose structured errors; apps just need context chains.std convention for composable .context() chains.? propagation everywhere. Avoid match on Result when ? + .map_err() suffices.unwrap() in non-test code. Use expect("reason") only for proven invariants.#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("invalid token at position {position}")]
InvalidToken { position: usize, token: char },
#[error("unexpected end of input")]
UnexpectedEof,
#[error(transparent)]
Io(#[from] std::io::Error),
}
Adapted from Domain-Driven Design (Evans/Fowler) for Rust.
Name types, functions, and modules in the language of the problem domain, not the implementation.
// bad — describes implementation mechanics
fn find_end_offset(s: &str) -> Option<usize>
fn check_string(s: &str) -> bool
// good — describes domain concepts
fn PaymentResult::validate(invoice: &Invoice) -> Option<PaymentResult>
fn ClassName::is_tailwind(token: &str) -> bool
Domain values without identity are structs. Functions take &Struct and return new structs — no mutation through output parameters.
// bad — caller provides mutable buffer
fn process(input: &Config, out: &mut String)
// good — function returns a value object
fn process(input: &Config) -> ProcessResult
When a domain has a fixed set of variants or checks, use an enum — not trait objects, not loose functions.
// bad — scattered functions, no unifying type
fn is_not_empty(s: &str) -> bool { ... }
fn starts_with_letter(s: &str) -> bool { ... }
// good — enum variants are self-documenting and composable
enum ValidationRule { NonEmpty, StartsWithLetter, ContainsHyphen }
impl ValidationRule {
fn passes(self, input: &str) -> bool { match self { ... } }
}
const RULES: &[ValidationRule] = &[ValidationRule::NonEmpty, ...];
Use traits when behavior needs to be extended by future implementations. Start with the trait, then implement concrete types.
// bad — parallel standalone functions
fn run_git_cmd() -> Output { ... }
fn run_uv_cmd() -> Output { ... }
// good — shared trait, separate implementations
trait ExternalCommand {
fn execute(&self) -> Result<Output>;
}
impl ExternalCommand for Git { ... }
impl ExternalCommand for Uv { ... }
See also Service & middleware and Trait object plugin in Ecosystem patterns.
Each Rust module is a bounded context. Types and functions within a module share a domain model; the module boundary is the public API. Keep internal helpers private.
Source: sans-io.readthedocs.io
Protocol logic (parsing, validation, state machines, data transformation) must be pure functions or types that take data in and return data out — no sockets, no channels, no async, no file handles. I/O operations (network, channels, disk) live in a thin outer layer that calls the protocol layer.
This makes protocol logic testable without standing up real infrastructure, reusable across different I/O backends (tokio, crossbeam, sync), and composable across boundaries (Rust ↔ Python).
// bad — protocol parsing entangled with channel I/O
impl SlotSend {
fn __call__(&self, py, event: &PyDict) -> PyResult<...> {
let type_val: String = event.get_item("type")?.extract()?;
match type_val.as_str() {
"http.response.start" => {
let status = event.get_item("status")?.extract()?;
*self.status.lock() = Some(status);
// parse headers here too...
}
"http.response.body" => {
let body = event.get_item("body")?.extract()?;
self.outbound_tx.send(OutboundSlot { ... })?; // I/O mixed in
self.body_tx.send(body)?; // I/O mixed in
}
}
}
}
// good — protocol layer is a pure function, I/O layer calls it
enum SendEvent {
Start { status: u16, headers: Vec<(Bytes, Bytes)> },
Body { data: Bytes, more_body: bool },
}
fn parse_send_event(event: &Bound<'_, PyDict>) -> PyResult<SendEvent> {
// pure — no channels, no async, testable with synthetic PyDicts
}
impl SlotSend {
fn __call__(&self, py, event: &PyDict) -> PyResult<...> {
let parsed = parse_send_event(event)?; // protocol
self.dispatch(parsed) // I/O
}
}
The same principle applies to request classification:
// bad — routing decision mixed with hyper I/O
async fn handle(self, req: Request<Incoming>) -> Response<...> {
if req.uri().path() == "/_health/alive" {
return json_response(HEALTH_ALIVE); // mixed: decision + response construction
}
if is_websocket_upgrade(&req) {
return self.dispatch.dispatch_ws(req).await; // mixed: decision + dispatch
}
// ... semaphore, timeout, dispatch ...
}
// good — classification is a pure function
enum RequestKind {
Probe(ProbeKind),
WebSocket,
Http,
}
fn classify(path: &str, headers: &HeaderMap) -> RequestKind {
// pure — no async, no Response construction, testable with strings
}
async fn handle(self, req: Request<Incoming>) -> Response<...> {
match classify(req.uri().path(), req.headers()) {
RequestKind::Probe(kind) => probe_response(kind),
RequestKind::WebSocket => self.dispatch.dispatch_ws(req).await,
RequestKind::Http => self.dispatch_http(req).await,
}
}
Rule of thumb: if a function touches both data transformation AND a channel/socket/file, split it. The data transformation half is the protocol layer; the channel/socket half is the I/O layer. The protocol layer should be testable with #[test] using synthetic inputs — no #[tokio::test], no channels, no Python::attach.
Sources: Microsoft M-INIT-BUILDER, M-IMPL-ASREF, M-IMPL-IO, M-AVOID-WRAPPERS; Rust API Guidelines C-COMMON-TRAITS
impl AsRef<str> / impl AsRef<Path> over concrete &str/String/&Path in function params when callers may have either type.impl Read / impl Write for I/O functions. Decouples logic from concrete I/O sources — enables testing with Cursor<Vec<u8>>.Arc<Mutex<T>>, Box<T>, Rc<T> — let callers choose their wrapping strategy.Debug, Clone, PartialEq, Default on all public types. (C-COMMON-TRAITS)Debug. No exceptions. (M-PUBLIC-DEBUG)Copy. Do not derive or implement Copy unless the type genuinely benefits from implicit copy semantics. Prefer Clone with explicit .clone() so copies are visible and intentional.Functions return domain types instead of writing into &mut parameters. This makes data flow explicit and enables composition via .map(), .fold(), iterators.
// bad — mutation hides data flow
fn transform(input: &str, out: &mut Vec<String>)
// good — return value makes flow explicit
fn transform(input: &str) -> Vec<TransformResult>
Split complex validation into a scanning step and a checking step. Each is its own function. No nesting beyond one level.
// bad — nested ifs, multiple concerns in one block
if b == CLOSE {
if i + 1 < len && bytes[i + 1] == CLOSE {
if i == 0 { return None; }
return Some(i);
}
return None;
}
// good — scan finds candidate, validate checks it
fn scan(input: &str) -> Option<Boundary> { ... } // walks bytes
fn validate(pos: usize, bytes: &[u8]) -> Option<Boundary> { ... } // checks invariants
Prefer .iter(), .map(), .filter(), .collect() over for i in 0..len with manual indexing. Iterator chains are bounds-checked by construction.
Cow<'a, str> for conditional ownershipWhen a function sometimes borrows and sometimes allocates, return Cow<'a, str> instead of always cloning.
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
Use String::with_capacity() / Vec::with_capacity() when the final size is known or estimable. Avoids repeated reallocations.
If a function parses, validates, classifies, or transforms data — it must not touch I/O (channels, sockets, files, async runtimes). Accept data in, return data out. This makes protocol logic testable with #[test] (no #[tokio::test], no Python::attach, no channel setup).
// bad — needs a channel + tokio runtime to test
async fn handle_response(data: ResponseData, tx: &mpsc::Sender<Bytes>) -> Result<()> {
let status = StatusCode::from_u16(data.status)?;
for chunk in data.chunks { tx.send(chunk).await?; } // I/O inside logic
Ok(())
}
// good — pure transformation, separately testable
fn build_response(data: ResponseData) -> Result<OutboundResponse> {
let status = StatusCode::from_u16(data.status)?;
Ok(OutboundResponse { status, headers: data.headers, body: data.body })
}
When hitting a bug, write a failing test that reproduces it first. Only then write the fix. Tests document the exact failure mode and prevent regressions.
Production Rust relies on patterns popularized by Tokio, Axum, Tower, and Serde. These bridge the gap between the micro-level code patterns above and full application architecture.
Source: Rust API Guidelines C-BUILDER
Separate construction from representation. A builder accumulates configuration through method chaining and produces the final value in a terminal .build() call that can validate and fail.
Expands API design rule: "Builder pattern for complex initialization."
pub struct ServerConfig {
bind_addr: SocketAddr,
workers: usize,
tls: Option<TlsConfig>,
}
pub struct ServerConfigBuilder {
bind_addr: Option<SocketAddr>,
workers: usize,
tls: Option<TlsConfig>,
}
impl ServerConfigBuilder {
pub fn new() -> Self {
Self { bind_addr: None, workers: 1, tls: None }
}
pub fn bind_addr(mut self, addr: SocketAddr) -> Self {
self.bind_addr = Some(addr);
self
}
pub fn workers(mut self, n: usize) -> Self {
self.workers = n;
self
}
pub fn tls(mut self, config: TlsConfig) -> Self {
self.tls = Some(config);
self
}
pub fn build(self) -> Result<ServerConfig, ConfigError> {
let bind_addr = self.bind_addr.ok_or(ConfigError::MissingBindAddr)?;
Ok(ServerConfig { bind_addr, workers: self.workers, tls: self.tls })
}
}
Use when:
Avoid when:
new() with 1-3 required fields sufficesSource: Rust Design Patterns — Newtype
Wrap a primitive in a single-field tuple struct to enforce domain invariants at construction time. The inner value is private; access goes through validated constructors and accessor methods.
Complements Safety rule #9: "Domain types over raw primitives."
// from crates/framework/src/route.rs
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QualName(String);
#[derive(Debug, thiserror::Error)]
pub enum QualNameError {
#[error("qualified name must not be empty")]
Empty,
#[error("qualified name has empty segment: {0}")]
EmptySegment(String),
#[error("invalid qualified name segment: {0}")]
InvalidSegment(String),
}
impl QualName {
pub fn new(name: impl Into<String>) -> Result<Self, QualNameError> {
let name = name.into();
if name.is_empty() {
return Err(QualNameError::Empty);
}
for segment in name.split('.') {
if segment.is_empty() {
return Err(QualNameError::EmptySegment(name));
}
// ... validate each segment
}
Ok(Self(name))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
Use when:
String, u64, Vec<u8>) has domain constraints (non-empty, positive, valid format)Avoid when:
let binding instead)Source: Rust API Guidelines C-EXT
Add methods to a foreign type (one you don't own) by defining a trait and implementing it for that type. Callers use the trait to get the new methods.
// from crates/mcp/src/tools/mod.rs
pub trait ToolResultExt {
fn from_serializable(value: &impl StructuredObject) -> Self;
fn from_serializable_error(value: &impl StructuredObject) -> Self;
}
impl ToolResultExt for CallToolResult {
fn from_serializable(value: &impl StructuredObject) -> Self {
build_structured_result(value, false)
}
fn from_serializable_error(value: &impl StructuredObject) -> Self {
build_structured_result(value, true)
}
}
// Usage: CallToolResult::from_serializable(&my_response)
Use when:
*Ext)Avoid when:
Source: Cliffle — Rust Typestate Pattern
Encode protocol states as zero-sized type parameters. Methods that are only valid in a specific state are only available on that parameterization. State transitions consume the old value and return a new one.
pub struct Disconnected;
pub struct Connected;
pub struct Connection<S> {
addr: SocketAddr,
_state: std::marker::PhantomData<S>,
}
impl Connection<Disconnected> {
pub fn new(addr: SocketAddr) -> Self {
Self { addr, _state: PhantomData }
}
pub async fn connect(self) -> Result<Connection<Connected>, io::Error> {
// ... establish connection ...
Ok(Connection { addr: self.addr, _state: PhantomData })
}
}
impl Connection<Connected> {
pub async fn send(&self, data: &[u8]) -> Result<(), io::Error> {
// only available when connected
Ok(())
}
}
Use when:
Avoid when:
Source: The Little Book of Rust Macros
Declarative (macro_rules!) or procedural macros that auto-derive trait implementations or generate boilerplate. Used sparingly, they eliminate repetitive patterns that generics and traits alone cannot.
Controlled exception to Safety rule #8: "Macros sparingly." Derive macros are acceptable when the pattern is mechanical, repeated across many types, and error-prone to write by hand.
// from crates/mcp/src/tools/mod.rs
macro_rules! tool_response {
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
$( $(#[$field_meta:meta])* $field_vis:vis $field:ident : $ty:ty ),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, serde::Serialize)]
$vis struct $name {
$( $(#[$field_meta])* $field_vis $field : $ty, )*
}
impl $crate::tools::StructuredObject for $name {}
};
}
// Usage — every tool response struct gets Serialize + StructuredObject:
// tool_response! { pub struct MyToolOutput { pub result: String } }
Use when:
StructuredObject marker above)Avoid when:
#[derive(...)] from serde/thiserror already handle itSource: Tower — Service trait
A service is a trait with a single async call method that transforms a request into a response. Middleware wraps an inner service, adding cross-cutting behavior (logging, auth, timeouts) without modifying business logic. Services compose into layered stacks.
Architectural application of DDD "Traits for open abstractions." See also Trait object plugin.
// from crates/framework/src/bridge/dispatch.rs
// The trait defines the service contract:
pub trait HandlerDispatch: Send + Sync + std::fmt::Debug {
fn handle(
&self,
route: Arc<BoundRoute>,
app_state: Arc<AppState>,
request: InboundRequest,
) -> Pin<Box<dyn Future<Output = Result<OutboundResponse, AppError>> + Send>>;
}
// A concrete service implements the trait:
pub struct RequestResponseDispatch;
impl HandlerDispatch for RequestResponseDispatch {
fn handle(
&self,
route: Arc<BoundRoute>,
app_state: Arc<AppState>,
mut request: InboundRequest,
) -> Pin<Box<dyn Future<Output = Result<OutboundResponse, AppError>> + Send>> {
Box::pin(async move {
let ctx = extract_context(&mut request, &route, &app_state).await?;
let result = invoke_handler(&route, &ctx).await?;
Python::attach(|py| serialize_result(py, &result, &route))
})
}
}
// Layered call chain:
// axum handler → transport conversion → HandlerDispatch trait → Python bridge
Use when:
Avoid when:
Source: Rust Reference — async fn in traits
Before Rust 1.75, async methods in traits required returning Pin<Box<dyn Future<...> + Send>> manually. Since 1.75, async fn works directly in traits, but boxed futures remain necessary when you need trait objects (dyn Trait).
See also Async & concurrency.
// Pre-1.75 style (still needed for dyn dispatch):
// from crates/framework/src/bridge/dispatch.rs
pub trait HandlerDispatch: Send + Sync {
fn handle(
&self,
route: Arc<BoundRoute>,
app_state: Arc<AppState>,
request: InboundRequest,
) -> Pin<Box<dyn Future<Output = Result<OutboundResponse, AppError>> + Send>>;
}
// Post-1.75 style (when dyn dispatch is not needed):
pub trait Processor: Send + Sync {
async fn process(&self, input: &[u8]) -> Result<Vec<u8>>;
}
Use when:
dyn Trait (e.g. stored in a Vec<Box<dyn Trait>>) — boxed futures are requiredSend bounds on the future for multi-threaded runtimesAvoid when:
impl Trait / T: Trait) — use native async fn directlyasync-trait crate and its overhead is acceptableSource: Rust Design Patterns — Strategy
Store Arc<dyn Trait> in a registry to support open-ended extension at runtime. Each plugin implements a shared trait. The registry dispatches by name or type, enabling plugin-style architectures.
Architectural application of DDD "Traits for open abstractions."
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn init(&self) -> Result<()>;
fn handle(&self, input: &[u8]) -> Result<Vec<u8>>;
}
pub struct PluginRegistry {
plugins: Vec<Arc<dyn Plugin>>,
}
impl PluginRegistry {
pub fn register(&mut self, plugin: Arc<dyn Plugin>) {
self.plugins.push(plugin);
}
pub fn dispatch(&self, name: &str, input: &[u8]) -> Result<Vec<u8>> {
let plugin = self.plugins.iter()
.find(|p| p.name() == name)
.ok_or_else(|| anyhow!("unknown plugin: {name}"))?;
plugin.handle(input)
}
}
Use when:
Avoid when:
matchSource: Rust Book — Drop trait
Implement Drop on a wrapper type to guarantee cleanup when the value goes out of scope — even on panics or early returns. The guard owns the resource and releases it deterministically.
// from crates/framework/src/bridge/streaming.rs
impl Drop for AsgiBodyStream {
fn drop(&mut self) {
if let Some(task) = self.handler_task.take() {
task.abort();
}
}
}
// The pattern: wrap a resource in a guard that cleans up on drop
pub struct TempDir {
path: PathBuf,
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
Use when:
Avoid when:
.close() method insteadArc) makes the drop point unpredictable — consider explicit shutdown signalsSources: Microsoft M-CANONICAL-DOCS, M-FIRST-DOC-SENTENCE, M-MODULE-DOCS, M-DOC-INLINE
/// is the summary — keep it tight./// Parses a TOML configuration file into a validated config.
# Examples, # Errors, # Panics, # Safety.//! on every public module. Explain what the module provides and when to use it.#[doc(inline)] on re-exports to surface docs at the re-export site, not buried in submodules.Sources: Microsoft M-YIELD-POINTS; Cloudflare clippy::await_holding_lock
std::thread::sleep(), no blocking I/O, no heavy CPU work in async context. Use tokio::task::spawn_blocking() instead.tokio::task::yield_now().await periodically (every ~10-100us of CPU work) to avoid starving other tasks. (M-YIELD-POINTS).await points. A MutexGuard held across an .await can deadlock or block the runtime. Scope the lock, copy data out, then await.// bad — guard held across await
let data = lock.lock().expect("lock poisoned");
send(data.clone()).await;
// good — lock released before await
let data = { lock.lock().expect("lock poisoned").clone() };
send(data).await;
tokio::select! for concurrent operations with cancellation semantics.std::sync::OnceLock over lazy_static! or once_cell::sync::Lazy for one-time initialization.Treat Python as an external runtime boundary, not a normal library. The bridge/ module is the canonical boundary between Rust domain logic and the Python interpreter — all PyO3 usage is contained there.
Architectural application of DDD "Modules as bounded contexts." The Python boundary is a bounded context with its own adapter layer.
Only boundary modules may import PyO3 types (PyObject, Py<PyAny>, Python<'py>). Everything outside the boundary uses Rust domain types. The data pipeline is:
InboundRequest → PythonAdapter → ASGI callable → PythonAdapter → OutboundResponse
Rust domain types (InboundRequest, OutboundResponse, AppState) flow through the system. PyO3 conversions happen at the boundary, not deep in business logic.
Rule: If a module outside bridge/ needs to touch Python, it belongs in the bridge or needs a new adapter.
Wrap Py<PyAny> and Py<PyString> in semantic newtypes when they cross function boundaries. Raw Py<PyAny> in a function signature says nothing about what the object is.
Complements Newtype pattern: same principle, applied to Python references.
// bad — raw Python reference with no domain meaning
fn call_app(app: &Py<PyAny>, scope: &Py<PyAny>) -> PyResult<Py<PyAny>>
// good — semantic types document the protocol
struct AsgiApp(Py<PyAny>);
struct AsgiScope(Py<PyDict>);
struct AsgiReceive(Py<PyAny>);
fn call_app(app: &AsgiApp, scope: &AsgiScope) -> PyResult<AsgiReceive>
Group related interned Python objects into a struct rather than passing individual Py<PyString> values:
// from crates/framework/src/bridge/asgi.rs — ScopeInterns
pub struct ScopeInterns {
pub type_key: Py<PyString>,
pub asgi_key: Py<PyString>,
pub http_version_key: Py<PyString>,
// ...
}
Core rule: Python objects must not be constructed in the hot request path. All Python objects must either be cached, pooled, or reused.
Three strategies:
pyo3::intern! or pre-build Py<PyString> at startup.dict.copy() + mutate per-request fields. Cheaper than PyDict::new() + full population.// bad — allocates a new dict and all keys on every request
fn build_scope(py: Python<'_>, request: &InboundRequest) -> PyResult<Py<PyDict>> {
let scope = PyDict::new(py);
scope.set_item("type", "http")?; // allocates "type" string
scope.set_item("asgi", asgi_dict)?; // allocates "asgi" string
// ... 15 more keys
Ok(scope.into())
}
// good — copy pre-built template, mutate only per-request fields
fn scope_from_template(
py: Python<'_>,
template: &Py<PyDict>,
request: &InboundRequest,
interns: &ScopeInterns,
) -> PyResult<Py<PyDict>> {
let scope = template.bind(py).call_method0(intern!(py, "copy"))?;
scope.set_item(&interns.path_key, &request.path)?; // only per-request data
Ok(scope.unbind())
}
Use Vec::with_capacity / pre-sized PyList + append over building collections from scratch.
Cache Python attributes at startup in a struct, not via per-call getattr. Every getattr does a dict lookup on the Python side.
// bad — resolves attribute on every call
fn schedule(py: Python<'_>, loop_obj: &Py<PyAny>, callback: Py<PyAny>) -> PyResult<()> {
loop_obj.call_method1(py, "call_soon_threadsafe", (callback,))?;
Ok(())
}
// good — resolve once, use cached reference
pub struct EventLoopHandle {
call_soon_threadsafe: Py<PyAny>,
create_task: Py<PyAny>,
// ... other cached callables
}
impl EventLoopHandle {
fn schedule(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
self.call_soon_threadsafe.call1(py, (callback,))?;
Ok(())
}
}
Use pyo3::intern! for string keys used in dict operations (set_item, get_item). The macro caches the Python string across calls.
#[pyclass] creates Python heap objects — every instantiation goes through Python's allocator. In request-handling hot paths, prefer alternatives:
PyCFunction::new_closure for one-shot callables. Creates a Python callable from a Rust closure without a #[pyclass] heap allocation.#[pyo3(freelist = N)] when a pyclass is unavoidable in the hot path. Maintains a free list of pre-allocated instances.ResolvedAwaitable) over pyo3_async_runtimes::tokio::future_into_py when the result is already available synchronously.// bad — allocates a new pyclass on every request
#[pyclass]
struct Callback { inner: Box<dyn FnOnce()> }
// good — closure avoids heap allocation
let callback = PyCFunction::new_closure(py, None, None, move |args, _kwargs| {
// handle the callback
Ok(())
})?;
See RAII guard for cleanup patterns on pyclass objects that own resources.
The GIL is the single biggest contention point at the Python boundary. Rules:
.await. This extends the async rule about locks — the GIL is a global lock.call_soon_threadsafe. Brief GIL on the tokio worker (to enqueue), heavy Python work on the event loop thread.Python::attach calls that could be one are wasted overhead.// bad — holds GIL while doing work
Python::attach(|py| {
let result = expensive_python_call(py)?; // long hold
process_result(py, &result)?; // still holding
schedule_next(py, &result)?; // still holding
Ok(())
})
// good — brief GIL to enqueue, work happens on event loop thread
Python::attach(|py| {
let callback = prepare_callback(py, &data)?; // brief: build closure
event_loop.schedule(py, callback)?; // brief: enqueue
Ok(())
})
// Heavy Python work runs on the event loop thread, not under our GIL hold
Each worker owns its own: interpreter, event loop, symbol cache, and object pools. Nothing crosses worker boundaries — there is no GIL contention between workers.
Worker-scoped Python state lives in AppState. This includes interned strings, scope templates, cached callables, and connection pools. Initialize all of it once during worker startup.
Rule: Never pass Py<T> between workers. If data must cross workers, serialize to Rust types first.
Mental model: Rust sends a request to the Python runtime and receives a response. It does not "call Python functions" — it dispatches work across a boundary.
Consequences:
Python::attach / call_method is an RPC call with overhead. Coalesce where possible.Anti-pattern: Multiple sequential Python::attach calls that could be a single attach block:
// bad — three boundary crossings
Python::attach(|py| build_scope(py, &req))?;
Python::attach(|py| call_app(py, &scope))?;
Python::attach(|py| read_response(py, &result))?;
// good — one boundary crossing
Python::attach(|py| {
let scope = build_scope(py, &req)?;
let result = call_app(py, &scope)?;
read_response(py, &result)
})
Sources: Microsoft M-STATIC-VERIFICATION; Cloudflare foundations
Cargo.toml lint config:[lints.rust]
unreachable_pub = "warn"
[lints.clippy]
unwrap_used = "warn"
clone_on_ref_ptr = "warn"
await_holding_lock = "deny"
large_futures = "warn"
#[expect] over #[allow], always with reason. #[expect] warns when the suppression is no longer needed — prevents lint rot.cargo clippy -- -D warnings as the zero-warnings gate. No code merges with clippy warnings.test_<unit>_<scenario>_<expected> format: test_parse_empty_input_returns_error.tempfile for filesystem tests. Never write to hardcoded paths or the working directory.Send/Sync at compile time for types that cross thread boundaries:fn _assert_send<T: Send>() {}
fn _assert_sync<T: Sync>() {}
#[test]
fn connection_pool_is_send_sync() {
_assert_send::<ConnectionPool>();
_assert_sync::<ConnectionPool>();
}
Display output and verify .source() chains — error messages are part of the public API.#[should_panic(expected = "...")] for panic tests — always include the expected substring.