`getattr_opt` uses identity check instead of subclass check for `AttributeError` on Python < 3.13
`PyAnyMethods::getattr_opt` on Python < 3.13 uses `.is()` (type identity) instead of `is_subclass_of` to check for `AttributeError`. This means that subclasses of `AttributeError` are not treated as "attribute not found" and are instead propagated as errors.
On Python 3.13+, `getattr_opt` uses the C API `PyObject_GetOptionalAttr`, which correctly handles `AttributeError` subclasses. The behavior difference between Python versions is unexpected.
This was discovered via a regression in pydantic (pydantic/pydantic#13092), where Django's `RelatedObjectDoesNotExist` (a subclass of `AttributeError`) is raised by a property and should be treated as a missing attribute.
The bug is in `src/types/any.rs`, in the `#[cfg(not(Py_3_13))]` branch of `getattr_opt`:
```rust
#[cfg(not(Py_3_13))]
{
match any.getattr(attr_name) {
Ok(bound) => Ok(Some(bound)),
Err(err) => {
let err_type = err
.get_type(any.py())
.is(PyType::new::<PyAttributeError>(any.py()));
// ^^^ identity check — `type(err) is AttributeError`
// should be a subclass check — `isinstance(err, AttributeError)`
match err_type {
true => Ok(None),
false => Err(err),
}
}
}
}
```
`.is()` returns `true` only when the error type is exactly `AttributeError`, not when it's a subclass. The fix should use `is_subclass_of::<PyAttributeError>()` (or equivalent) to match the semantics of the Python 3.13+ code path.
### Steps to Reproduce
1. Define a Python class with a property that raises an `AttributeError` subclass:
```python
class MissingRelation(AttributeError):
pass
class Obj:
@property
def child(self):
raise MissingRelation("missing child")
```
2. Call `getattr_opt` on an instance of `Obj` for the `child` attribute (from Rust via PyO3).
3. On Python < 3.13: `getattr_opt` returns `Err(MissingRelation)` instead of `Ok(None)`.
4. On Python 3.13+: `getattr_opt` correctly returns `Ok(None)`.
### Backtrace
```shell
N/A — no panic, just incorrect error propagation.
```
### Your operating system and version
macOS 26.3.1 (arm64)
### Your Python version (`python --version`)
Python 3.12.x (reproduces on any Python < 3.13)
### Your Rust version (`rustc --version`)
rustc 1.87.0
### Your PyO3 version
0.28.3
### How did you install python? Did you use a virtualenv?
pyenv, with virtualenv
### Additional Info
The suggested fix is to change the `#[cfg(not(Py_3_13))]` fallback in `getattr_opt` from:
```rust
let err_type = err.get_type(any.py()).is(PyType::new::<PyAttributeError>(any.py()));
```
to something like:
```rust
let err_type = err.get_type(any.py()).is_subclass_of::<PyAttributeError>()?;
```
This would make the pre-3.13 fallback match the semantics of `PyObject_GetOptionalAttr` on Python 3.13+.
Upstream pydantic issue: pydantic/pydantic#13092
Pydantic PR that exposed this: pydantic/pydantic#12571
0 条评论