Files
RustPython/AGENTS.md
Jeong, YunWon bdc9246b37 Address builtin members by field offset (#8816)
* Address builtin members by field offset

`#[pymember]` now declares a field of the payload and generates a
PyMemberDef whose offset is the payload offset plus the field offset.
The getter/setter function form and `MemberAccess::Func` are removed;
builtin, slot, and C-API members share one get/set path. The macro
checks the field type against the member type.

Converted members: property fget/fset/fdel/__doc__, function
__globals__/__closure__/__builtins__/__doc__/__module__, staticmethod
and classmethod __func__/__wrapped__, complex real/imag (T_DOUBLE),
frame f_trace_lines (T_BOOL), defaultdict default_factory, getset and
member descriptor __name__/__objclass__, module __dict__.
frame.f_trace_opcodes is a getset.

Writable member fields are atomic pointer cells. The instance dict
pointer is an atomic cell at a fixed offset in front of the object,
and dict presence follows the type's HAS_DICT flag. The pickle
basicsize check no longer counts dict, weakref, and slot storage,
which live outside the payload.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Address member review findings

- `#[pymember]` keeps leading `#[cfg]` attributes on the generated
  registration, and malformed `field`/`offset` values are compile
  errors.
- The `descriptor` module and the items the expansion names are public.
- Writable double members are an `AtomicU64` cell, loaded and stored
  with relaxed atomics. Readonly double members stay plain `f64` reads.
- `LOAD_ATTR_SLOT` is not specialized for `Py_AUDIT_READ` members.
- `defaultdict.__reduce__` passes no factory argument when
  `default_factory` is None.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Declare members on payload fields

`#[pymember]` goes on a payload struct field, or on the struct with an
explicit `offset` when the storage is not a payload field. The struct
`#[pyclass]` builds `PyClassDef::MEMBERS`, a static `PyMemberSpec`
table, and `extend_class` registers one member descriptor per entry.
`path` addresses a subfield, one field may carry several members, and a
`#[cfg]` on the field gates its entry. The impl-side const form is
removed and now a compile error.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Split nullable and non-null atomic cells

The nullable cell API (`new_empty`, `load_owned`, `store`,
`compare_exchange_empty`) moves from `PyAtomicRef<PyObject>` to
`PyAtomicRef<Option<PyObject>>`, and the typed `PyAtomicRef<Option<T>>`
shares it. The non-null `PyAtomicRef<PyObject>` gets `load_owned` and
`store` that return the object directly.

Slot cells, property fget/fset/fdel/__doc__, and defaultdict
default_factory are nullable cells. staticmethod and classmethod
callable are non-null cells and are read without `unwrap`. The
instance dict cell is `PyAtomicRef<Option<PyDict>>`, so reading it no
longer downcasts.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Drop the unused cold trace_lines flag

`FrameColdData::trace_lines` had no writer after `f_trace_lines`
became a frame object member. A frame without a frame object traces
lines by default.

Assisted-by: Claude:claude-opus-5-5

* Compute payload offsets from the merged Py header

`PyInner` is now `Py`, so the payload offset and the dict member
offset are measured from `Py<T>`.

Assisted-by: Claude:claude-opus-5-5

* Expose descriptor names and owner types as members

method_descriptor, classmethod_descriptor, and wrapper_descriptor
expose `__objclass__` and `__name__` as readonly members, and
method-wrapper exposes `__self__` as a readonly member. The separate
`objclass` field of method and classmethod descriptors always equaled
`common.typ` and is removed.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Expose remaining builtin tp_members as members

Attributes stored in a payload field become readonly or writable
member descriptors: bound method __func__/__self__, code
co_argcount/co_posonlyargcount/co_kwonlyargcount/co_stacksize (Py_T_UINT),
generic alias, coroutine and async generator members, interpolation and
template, range and slice start/stop/step, type __base__, union, and the
typing TypeVar, ParamSpec, TypeVarTuple, and TypeAliasType members.
`MemberKind` gains `Int` and `Uint` with the `PyMember_GetOne`/`SetOne`
range checks.

Attributes computed on access or kept behind a lock stay getsets.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Spell a docless member as doc = false

`#[pymember(doc = false)]` stores no docstring, matching the
`doc = "..."` form, and replaces the separate `no_doc` flag.

Assisted-by: Claude:claude-opus-5-5

* Make pymember readonly by default

`#[pymember]` declares a readonly member unless `writable` is given.
The `readonly` option is removed and every declaration is converted.

Assisted-by: Claude:claude-opus-5-5

* Check the receiver of method descriptors

Binding or calling a method_descriptor with an object that is not an
instance of the owner type raises TypeError before the native function
runs. Static and class methods are exempt.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Expose exception attributes as members

BaseException.__suppress_context__, StopIteration.value, SystemExit.code,
ImportError, NameError, AttributeError, OSError, SyntaxError,
BaseExceptionGroup and the UnicodeEncode/Decode/TranslateError attributes
are member descriptors backed by payload fields. ImportError, NameError,
AttributeError and SyntaxError gain payloads for their fields, and the
methods that read those attributes read the fields. Add the Py_ssize_t
member kind for UnicodeError start and end.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Avoid same-type casts in unsigned member conversion

On targets where c_ulong is u32, casting through c_ulong was a no-op
cast that clippy rejects.

Assisted-by: Claude:claude-opus-5-5

* Prefer an explicit pymember doc over the doc table

doc = "text" is used as the docstring without consulting the stored
attribute documentation; only an omitted doc looks it up. Drop the member
doc strings that repeated the stored documentation.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Infer pymember kinds from field types

MemberLayout gives the member kind of each accepted field type, and the
macro reads it from the field or path subfield, so type = "bool" and
the other numeric spellings are gone. type only selects object_ex.
Writable doubles use a new AtomicF64 cell instead of AtomicU64 bits.
MemberLayout and MemberCell replace the per-kind marker traits.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Read nullable atomic cells only through load_owned

PyAtomicRef<Option<T>> and PyAtomicRef<Option<PyObject>> lose to_owned,
to_owned_ordering and try_to_owned; load_owned, which retries the
incref until it applies to the pointer still in the cell, is the one
owned read. Callers that kept a deref borrow across VM calls now take a
strong reference.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Inherit static type slots from the whole MRO

extend_class copied slots only from the direct bases, and tp_init is
copied only from a base that defines it, so TabError missed the
SyntaxError init that IndentationError inherits. Walk the MRO the same
way init_slots does.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Let Rust doc comments win over stored docstrings

A /// comment on a Python-exposed item is its docstring; the
rustpython-doc entry is used only when there is none. #[pymember] reads
the field's /// comment instead of doc = "...". Doc comments that were
shadowed by a stored docstring become // comments, so no docstring
changes. AGENTS.md describes the rule.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Adapt members to upstream changes

_collections._tuplegetter.__doc__ is a writable nullable member on the
payload field. The impl-side const form no longer compiles.

Assisted-by: Grok:4.7

* Drop docstring text left as comments and inline downcast_cell

Comments that only repeated an item's docstring are removed; developer
notes stay as // comments. The typed cell methods call
downcast_unchecked directly, each with its own safety note.

Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5

* Read sqlite row factories through load_owned

The nullable cell no longer has to_owned; the sqlite module is built only
with the sqlite feature, so the default build did not catch it.

Assisted-by: Claude:claude-opus-5-5
2026-09-27 13:38:11 +09:00

12 KiB

AI Agent Instructions for RustPython

This document provides guidelines for AI coding agents (GitHub Copilot, Claude Code, Gemini, etc.) contributing to the RustPython project.

Project Overview

RustPython is a Python 3 interpreter written in Rust, implementing Python 3.14.0+ compatibility. The project aims to provide:

  • A complete Python-3 environment entirely in Rust (not CPython bindings)
  • A clean implementation without compatibility hacks
  • Cross-platform support, including WebAssembly compilation
  • The ability to embed Python scripting in Rust applications

Repository Structure

See the "Code organization" section in CONTRIBUTING.md for the current directory layout.

AI Agent Rules

CRITICAL: AI Policy

  • Follow RustPython's AI Policy for every AI-assisted contribution.
  • Disclose AI assistance in commit messages with an Assisted-by: AGENT_NAME:MODEL_VERSION trailer. Use one trailer per AI tool, and never use Co-authored-by for an AI assistant.

CRITICAL: Git Operations

  • NEVER create pull requests directly without explicit user permission
  • NEVER push commits to remote without explicit user permission
  • Always ask the user before performing any git operations that affect the remote repository
  • Commits can be created locally when requested, but pushing and PR creation require explicit approval

CRITICAL: Commit Hooks and Validation

  • Install the repository's pre-commit hook with prek install (or pre-commit install) after cloning the repository.
  • Every commit must run the configured pre-commit hook. NEVER bypass it with --no-verify. Automated workflows that use a normal git commit, such as scripts/update_lib quick, should be allowed to create local commits through the hook.
  • If a hook auto-fixes files (e.g. ruff-format, rustfmt), re-stage the fixes and retry the commit. Do not amend or force a failing commit through.
  • Before completing a task, run the tests appropriate for the change. Test commands are documented in the Testing section below. At minimum run cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi, then run cargo test from crates/capi; if the change touches extra_tests/snippets/ run pytest -v there too, and if it touches Lib/ or interpreter behavior, run the relevant cargo run --release -- -m test <module> modules.

Important Development Notes

Running Python Code

When testing Python code, always use RustPython instead of the standard python command:

# Use this instead of python script.py
cargo run -- script.py

# For interactive REPL
cargo run

# With specific features
cargo run --features jit

# Release mode (recommended for better performance)
cargo run --release -- script.py

Comparing with CPython

When you need to compare behavior with CPython or run test suites:

# Use python command to explicitly run CPython
python my_test_script.py

# Run RustPython
cargo run -- my_test_script.py

Working with the Lib Directory

The Lib/ directory contains Python standard library files copied from the CPython repository. Important notes:

  • These files should be edited very conservatively
  • Modifications should be minimal and only to work around RustPython limitations
  • Tests in Lib/test often use one of the following markers:
    • Add a # TODO: RUSTPYTHON comment when modifications are made
    • unittest.skip("TODO: RustPython <reason>")
    • unittest.expectedFailure with # TODO: RUSTPYTHON <reason> comment

Choosing the right marker

When marking a test that fails on RustPython, prefer one of the following forms:

@unittest.expectedFailure  # TODO: RUSTPYTHON; <reason>
# or
@unittest.expectedFailureIf(<condition>, "TODO: RUSTPYTHON; <reason>")

If the test would crash the interpreter (segfault, Rust panic, abort, infinite loop), use skip instead so the rest of the suite can still run:

@unittest.skip("TODO: RUSTPYTHON; <reason>")
# or
@unittest.skipIf(<condition>, "TODO: RUSTPYTHON; <reason>")

When to use which:

  • Prefer expectedFailure / expectedFailureIf by default. The test body still runs, so if RustPython is later fixed, the unexpected pass surfaces immediately and the decorator can be removed. Use the conditional *If form when the failure is environment-specific (e.g., a platform or build flag).
  • Use skip / skipIf only when running the test would take down the test process — segfaults, Rust panics, aborts, or hangs that block subsequent tests. Skipping keeps the suite usable; expectedFailure cannot help here, because the test body still executes.

To find WIP entries that are partly modified and may need follow-up:

grep -d recurse 'TODO: RUSTPYTHON' Lib/test/

Clean Build

When you modify bytecode instructions, a full clean is required:

rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r

Testing

# Run Rust unit tests
cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi

# Run C-API tests from their directory so their separate Cargo config applies
(cd crates/capi && cargo test)

# Run Clippy with the same C-API separation
cargo clippy --workspace --all-targets --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi
(cd crates/capi && cargo clippy --all-targets)

# Run Python snippets tests (debug mode recommended for faster compilation)
cargo run -- extra_tests/snippets/builtin_bytes.py

# Run all Python snippets tests with pytest
cd extra_tests
pytest -v

# Run the Python test module (release mode recommended for better performance)
cargo run --release -- -m test ${TEST_MODULE}
cargo run --release -- -m test test_unicode # to test test_unicode.py

# Run the Python test module with specific function
cargo run --release -- -m test test_unicode -k test_unicode_escape

Note: For extra_tests/snippets tests, use debug mode (cargo run) as compilation is faster. For unittest (-m test), use release mode (cargo run --release) for better runtime performance.

Determining What to Implement

Run ./scripts/whats_left.py to get a list of unimplemented methods, which is helpful when looking for contribution opportunities.

Coding Guidelines

Rust Code

  • Follow the default rustfmt code style (cargo fmt to format)
  • IMPORTANT: Always run clippy to lint code (cargo clippy) before completing tasks. Fix any warnings or lints that are introduced by your changes
  • Follow Rust best practices for error handling and memory management
  • Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Comments

  • Do not delete or rewrite existing comments unless they are factually wrong or directly contradict the new code.
  • Do not add decorative section separators (e.g. // -----------, // ===, /* *** */). Use /// doc-comments or short // comments only when they add value.
  • A /// doc comment on a Python-exposed item (#[pyattr], #[pyclass], #[pyfunction], #[pymethod], #[pyclassmethod], #[pystaticmethod], #[pygetset], #[pymember], #[pyslot], #[pyexception], #[pymodule], #[pystruct_sequence]) is that item's docstring and overrides the stored docs in rustpython-doc (crates/doc). Those stored docs are used only when the item has no /// comment. Developer notes on these items must be // comments.

Avoid Duplicate Code in Branches

When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once.

Bad:

let result = if condition {
    let msg = format!("message A: {x}");
    some_function(msg, shared_arg)
} else {
    let msg = format!("message B");
    some_function(msg, shared_arg)
};

Good:

let msg = if condition {
    format!("message A: {x}")
} else {
    format!("message B")
};
let result = some_function(msg, shared_arg);

Python Code

  • IMPORTANT: In most cases, Python code should not be edited. Bug fixes should be made through Rust code modifications only
  • Follow PEP 8 style for custom Python code
  • Use ruff for linting Python code
  • Minimize modifications to CPython standard library files

Integration Between Rust and Python

The project provides several mechanisms for integration:

  • pymodule macro for creating Python modules in Rust
  • pyclass macro for implementing Python classes in Rust
  • pyfunction macro for exposing Rust functions to Python
  • PyObjectRef and other types for working with Python objects in Rust

Common Patterns

Implementing a Python Module in Rust

#[pymodule]
mod mymodule {
    use rustpython_vm::prelude::*;

    #[pyfunction]
    fn my_function(value: i32) -> i32 {
        value * 2
    }

    #[pyattr]
    #[pyclass(name = "MyClass")]
    #[derive(Debug, PyPayload)]
    struct MyClass {
        value: usize,
    }

    #[pyclass]
    impl MyClass {
        #[pymethod]
        fn get_value(&self) -> usize {
            self.value
        }
    }
}

Adding a Python Module to the Interpreter

vm.add_native_module(
    "my_module_name".to_owned(),
    Box::new(my_module::make_module),
);

Building for Different Targets

WebAssembly

# Build for WASM
cargo build --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib --release

JIT Support

# Enable JIT support
cargo run --features jit

Linux Build and Debug on macOS

See the "Testing on Linux from macOS" section in CONTRIBUTING.md.

Building venvlauncher (Windows)

IMPORTANT: All 4 venvlauncher binaries use the same source code. Do NOT add multiple [[bin]] entries to Cargo.toml. Build once and copy with different names.

Test Code Modification Rules

CRITICAL: Test code modification restrictions

  • NEVER comment out or delete any test code lines except for removing @unittest.expectedFailure decorators and upper TODO comments
  • NEVER modify test assertions, test logic, or test data
  • When a test cannot pass due to missing language features, keep it as expectedFailure and document the reason
  • The only acceptable modifications to test files are:
    1. Removing @unittest.expectedFailure decorators and the upper TODO comments when tests actually pass
    2. Adding @unittest.expectedFailure decorators when tests cannot be fixed

Examples of FORBIDDEN modifications:

  • Commenting out test lines
  • Changing test assertions
  • Modifying test data or expected results
  • Removing test logic

Correct approach when tests fail due to unsupported syntax:

  • Keep the test as @unittest.expectedFailure
  • Document that it requires PEP 695 support
  • Focus on tests that can be fixed through Rust code changes only

CI Workflows

If you modify any file under .github/workflows/, the change must pass a zizmor scan in CI.

Documentation