Skip to content

fix: avoid #[pymethods] name collisions between regular methods and #[getter]/#[setter]/#[deleter]#6135

Open
mokashang wants to merge 4 commits into
PyO3:mainfrom
mokashang:fix/pymethod-name-collision-with-property
Open

fix: avoid #[pymethods] name collisions between regular methods and #[getter]/#[setter]/#[deleter]#6135
mokashang wants to merge 4 commits into
PyO3:mainfrom
mokashang:fix/pymethod-name-collision-with-property

Conversation

@mokashang

Copy link
Copy Markdown

Closes #5974.

Bug

The #[pymethods] macro generates an internal associated function for
every entry in the impl block. Up to now those names came from two
different conventions that overlap:

kind wrapper name
#[getter] fn x __pymethod_get_x__
#[setter] fn x __pymethod_set_x__
#[deleter] fn x __pymethod_delete_x__
regular method fn x __pymethod_x__

So defining both a property and a same-named accessor method — #[getter] fn x + fn get_x — collides on __pymethod_get_x__ with error[E0592]: duplicate definitions. The same trap exists for set_X against #[setter] and delete_X against #[deleter]. It also survives #[pyo3(name = "get_y")] renames, because the regular-method wrapper is keyed off spec.python_name, not the Rust identifier:

#[pyclass]
pub struct Object { x: u32, y: u32 }

#[pymethods]
impl Object {
    #[getter] fn x(&self) -> u32 { self.x }
    fn get_x(&self) -> u32 { self.x }                        // collision

    #[getter] fn y(&self) -> u32 { self.y }
    #[pyo3(name = "get_y")] fn y_get(&self) -> u32 { self.y } // collision
}
// error[E0592]: duplicate definitions with name `__pymethod_get_x__`
// error[E0592]: duplicate definitions with name `__pymethod_get_y__`

This blocks a common Python pattern: shipping a cheap value property alongside a parameterised get_value(...) method for callers that need the underlying computation. The original report (#5974) is a real adaptation case where backwards-compatible naming required exactly this shape.

Fix

Switch the wrapper name for regular methods from __pymethod_{python_name}__ to __pymethod_method_{python_name}__ in impl_py_method_def. The method infix can no longer be a substring of get_, set_, or delete_, so the prefix space is now disjoint regardless of which Python name the user chooses. This matches the direction agreed in #5974 (comment) — pick a distinct prefix for "ordinary" methods — and method reads more naturally than standard while having the same effect.

The change is purely internal: these __pymethod_*__ symbols are private associated items consumed by other macro-generated code in the same expansion. No public API moves.

Scope

I deliberately kept this narrow:

  • Only the regular-method site in pyo3-macros-backend/src/pymethod.rs is changed; the #[classattr] and gen_py_const sites also generate __pymethod_{name}__, but their collision space (uppercase-snake constants, class-level Python names) is much harder to hit in real code than get_X / set_X / delete_X. Happy to expand if reviewers prefer.
  • __pymethod___new____, __pymethod___richcmp____, __pymethod_traverse__, __pymethod_constructor__, __pymethod_variant_cls_* etc. are unchanged — they don't share the property-prefix shape.

Tests

  • Added property_and_regular_method_can_share_name_prefix in tests/test_getter_setter.rs. It exercises all three collision shapes — regular method (fn get_x), #[pyo3(name = "get_y")] fn y_get rename, and the setter variant fn set_z against #[setter] fn z — and verifies that each callable reaches the right Rust impl from Python (instance.x == 10 vs instance.get_x() == 11, etc.). Without the fix this test fails to compile with E0592.
  • Updated tests/ui/invalid_pymethods_duplicates.rs (+ .stderr snapshot): the existing "two methods both renamed to func" duplicate-detection still trips, just on __pymethod_method_func__ now.
  • cargo test -p pyo3 --tests is green across the test suite I can run locally on macOS (class basics, class attributes, getter/setter, methods, proto methods, multiple-pymethods feature, inheritance, declarative module, etc.). UI tests that fail on my machine all fail identically on main — they're rustc-version-sensitive snapshots and unrelated to this change.
  • cargo fmt --check clean; cargo clippy --tests --features=macros clean.

…[deleter] (fixes PyO3#5974)

Previously, `#[pymethods]` generated the same internal associated
function name for two distinct kinds of method:

- a getter for `x` (`#[getter] fn x`) becomes `__pymethod_get_x__`
  (the `get_` prefix is from `impl_py_getter_def`).
- a regular method called `get_x` becomes `__pymethod_get_x__` (the
  whole name comes from `impl_py_method_def`, which uses
  `__pymethod_{python_name}__`).

These clash with `error[E0592]: duplicate definitions with name
`__pymethod_get_x__``. The same pattern hits `set_X` against `#[setter]`
and `delete_X` against `#[deleter]`, and it survives `#[pyo3(name =
"get_y")]` because the regular-method wrapper is keyed off
`spec.python_name`.

This forces user-facing Python APIs to choose between common idioms
that should coexist — a `value` property *and* a separate `get_value`
method for parameterised lookup, which is a common pattern when
adapting legacy code.

Fix the regular-method wrapper to use a `__pymethod_method_*__` infix,
which can no longer collide with `__pymethod_get_*__`,
`__pymethod_set_*__`, or `__pymethod_delete_*__` regardless of the
chosen Python name. The maintainer-suggested direction in PyO3#5974 was to
add a distinct prefix that is not a substring of the property prefixes;
`method` reads more naturally than `standard` and has the same effect.

Add a regression test in `tests/test_getter_setter.rs` covering all
three collision shapes (regular method, `#[pyo3(name = ...)]` rename,
and the setter variant). Update the existing UI test that was asserting
on the old wrapper name.
@codspeed-hq

codspeed-hq Bot commented Jun 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 126 untouched benchmarks


Comparing mokashang:fix/pymethod-name-collision-with-property (1bbdc5f) with main (4c8407d)

Open in CodSpeed

Comment thread pyo3-macros-backend/src/pymethod.rs Outdated
let wrapper_ident = format_ident!("__pymethod_{}__", spec.python_name);
// The `method_` infix keeps the generated identifier from colliding with
// the wrappers for `#[getter]`/`#[setter]`/`#[deleter]`, which use the
// `get_`/`set_`/`delete_` prefixes (see #5974).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this kind of comment isn't really helpful. The one in the test is sufficient.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — dropped the comment in b07540f. The test docstring already covers it.

The regression test in test_getter_setter.rs already documents the
PyO3#5974 collision pattern in detail, so the in-source comment was
duplicating that explanation.
The wrapper identifier for regular #[pymethods] methods was renamed
to __pymethod_method_{name}__ to avoid colliding with getter/setter
wrappers (this PR's fix for PyO3#5974). One UI test snapshot still
referenced the old __pymethod_{name}__ symbol both in its //~ ERROR
annotation and in the .stderr file, so the trybuild check failed
on the python3.14t job.

Update the inline directive and snapshot to match the new symbol.
@mokashang

Copy link
Copy Markdown
Author

Pushed 1bbdc5f to fix the CI failure on python3.14t-x64.

The renamed wrapper identifier broke tests/ui/invalid_pyclass_generic — the test's //~ ERROR annotation and .stderr snapshot still spelled the old __pymethod___class_getitem____ symbol. Updated both to __pymethod_method___class_getitem____ so trybuild matches.

Verified locally: cargo test --test test_compile_error ... invalid_pyclass_generic now passes (1 passed; 63 filtered out).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Having an 'x' getter and a 'get_x' method leads to an internal naming conflict

2 participants