ADR-032: #[pyclass] Python naming convention — clean names; Py prefix is Rust-internal¶
Status: accepted.
Follows the ADR-026 precedent for converging divergent naming: policy + mechanical enforcement + direct renames for the existing population (no backward-compat alias shims), each rename in its own task.
Context¶
The PyO3 layer (the degenbot_rs cdylib, Python module degenbot._ffi)
has two coexisting conventions for registered #[pyclass] types:
Clean Python-facing names:
ArbitrageEngine,AlloyProvider,Contract,AsyncContract,AnvilFork,CancelHandle,BlockStream,PathIterator,LogFilter,AlloySubscription,Erc20TokenRow, the*Row/*RowInputtypes, and the typed exceptions.Py-prefixed visible names: the grandfather list in the Decision below (27 names at census time).
At adoption time the consumer layer split policy:
degenbot/dispatch/__init__.py stripped the prefix via alias re-exports
(stable companion names for driver code), while degenbot/bot/__init__.py
recommended the prefixed names (from degenbot.bot import Bot, PyBot, PyBotIo). New types had been picking a convention by drift, not by policy.
(The post-adoption VD5MD5 rename below removed the prefixed companion
names; the dispatch aliases now map clean FFI names.)
Decision¶
New
#[pyclass]types MUST use clean Python-facing names: leading capital, noPyprefix. (Py-prefixed Rust type names remain an acceptable Rust-internal convention; only the Python-facingname=registration is constrained here. The*_pypyfunction suffix stays where it marks the FFI seam — the companion packages strip it for driver code.)A
Py-prefixed Python-visible name is a Rust-internal seam type and may exist only on the grandfather list. The prefix is never extended.Grandfather list (runtime census 2026-08-17, 27 names):
PyAavePriceOracle,PyBalanceVectorView,PyBot,PyBotIo,PyChainlinkPriceFeed,PyCollateralPositionData,PyConcentratedLiquidityView,PyDatabasePositionQuery,PyDatabaseSnapshot,PyDebtPositionData,PyDexIdentity,PyDispatchCandidate,PyDispatchOutcome,PyDispatcher,PyDivergentPool,PyErc20Token,PyLiquidityPool,PyPayloadComposer,PyPool,PyReservePairView,PySimulateContext,PySolveResult,PySubscription,PySubmitCandidate,PyTxParams,PyTxSigner,PyUserPositionSummary.Retirement: task
VD5MD5(epicC7D2CH) renames the grandfather population to clean names per the ADR-026 precedent (direct rename, all import sites updated in-repo, no backward-compat aliases). Each rename removes the name from this list and from the gate test’s grandfather set in the same commit.Enforcement:
tests/ffi/test_ffi_registration_surface.pywalks the runtimedegenbot._ffimodule tree and asserts, in both directions, that the set of registeredPy-prefixed class names equals the grandfather list. A newPy-prefixed registration — or a dead list entry — fails the test.
Consequences¶
The
VD5MD5rename is mechanical and auditable: the list IS the scope.Companion packages (
degenbot.dispatch,degenbot.bot) keep working without alias churn until the rename lands; after it, prefix-stripping aliases become the vestige to remove.The gate is a runtime test over the built extension: it runs in the same
tests/ffisuite as the registration↔stub drift gate (taskDSWX6Z), so a renamed class that skips its stub update fails both gates.
Post-adoption: VD5MD5 rename complete (2026-08-17)¶
The grandfather list is empty: all 27 census names were renamed
(task VD5MD5, direct rename, no backward-compat aliases, per the ADR-026
precedent). Mapping:
Python name before |
Python name after |
Note |
|---|---|---|
|
|
the Python session class |
|
|
same collision reasoning |
|
|
the Python model class |
|
|
distinct from the Python |
|
|
the Python |
|
|
the Python |
|
|
mechanical |
|
|
mechanical (structural handle mirroring |
|
|
mechanical; |
|
|
mechanical; companion aliases in |
|
|
mechanical |
|
|
mechanical; |
|
|
mechanical (db analysis trio) |
|
|
mechanical (state views) |
Mechanically: the 15 pyclasses with explicit name = "PyX" attributes took
the new value; the 12 struct-default pyclasses gained an explicit
name = "X" (Rust struct names keep the Py qualifier — Rust-internal,
per D1). All Python import sites, companion alias imports (collapsed to
direct imports), .pyi stubs, tests, and examples were updated in the same
change; Python-visible repr/error-message strings in Rust were aligned.
Considered alternatives: keeping Py on the colliding names¶
Polars names its raw PyO3 layer PySeries / PyDataFrame, and that
pattern was the original precedent for this repo’s PyBot. It fits
Polars because those names are internal: polars.polars is not part
of the documented API, no example imports it, and the public surface is
the clean Python class (polars.Series). There, Py works as a
keep-out marker for internals, and “wrapping mechanism” is a fair label
for things nobody is meant to touch directly.
degenbot inverts that premise: degenbot._ffi is a documented,
first-class seam — .pyi-stubbed, drift-gated against its
registrations (task DSWX6Z), exercised by the README code-block (sybil)
gate, and imported directly by companion modules and shipped examples.
The seam’s names are public surface, so:
Pyis zero-information there: every name in the module is PyO3-managed, so the prefix disambiguates nothing and merely repeats the module path’s fact.the colliding names (
PyBotvs.degenbot.bot.Bot,PyErc20Tokenvs.degenbot.erc20.Erc20Token, …) collide on layer, not mechanism — Python driver class vs. Rust engine handle — so the informative qualifier is the layer origin:RustBotreads as “the Rust engine handle” at exactly the point where the reader crosses the boundary.Rust is the sole core implementation by architectural commitment (Rust is the engine; Python is a driver shell), so
Rustencodes a design promise rather than an implementation detail.
Rejected: keep PyBot etc. (Polars precedent). The precedent
depends on the raw layer staying hidden; degenbot’s seam is a public,
gated, documented surface, and the prefix there would be noise.
Rejected: uniform Rust* prefix on every handle. Over-prefixing —
clean names remain the default (D1); the prefix is reserved for names
where the clean form is already occupied by a Python type. The Mapping
table above lists each such case and why.
Post-adoption, round 2 (2026-08-17): the Rust prefix is retired¶
The five collision names kept their origin prefix under the D3 escape hatch. The fork decision retires the prefix entirely: the disambiguator is the module path, not the prefix.
Python name before |
after |
collision resolved by |
|---|---|---|
|
|
module path vs. |
|
|
no driver-side homonym |
|
|
module path vs. |
|
|
module path vs. the |
|
|
module path vs. |
D3’s collision escape (origin prefix on collision) is superseded. First-party code imports the handles directly from _ffi (the ADR-013 amendment); files that also bind a same-named Python class alias the import locally (e.g. _TokenHandle, _EngineSnapshot). The naming gate now asserts both the Py* and the Rust* registered-name sets are empty: no origin prefix of any kind, full stop.