Skip to content

[cppyy] Only alias size() to __len__ for container-like classes#21905

Merged
guitargeek merged 1 commit intoroot-project:masterfrom
chrisburr:fix/cppyy-size-len-pythonization
Apr 14, 2026
Merged

[cppyy] Only alias size() to __len__ for container-like classes#21905
guitargeek merged 1 commit intoroot-project:masterfrom
chrisburr:fix/cppyy-size-len-pythonization

Conversation

@chrisburr
Copy link
Copy Markdown
Contributor

Summary

If a C++ class has a size() method that returns a non-integer type cppyy adds a __len__ method but it always crashes. This can then affect the result of __bool__ in unexpected ways.

This PR fixes it by only pythonising size() -> __len__ if the return type is valid and it also appears to be iterable (has begin()+end() or operator[]).

Feel free to close this if you think there is a better solution.

This Pull request:

This was triggered by this issue in LHCb and Gaudi:

In order to understand the bootom issue, probably in cppyy), here is a simple reproducer, only based on Gaudi :

from GaudiPython import gbl as Cpp
intObj = Cpp.AnyDataWrapper[int](1)
print(bool(intObj))
print(bool(intObj.getData()))
dataObj = Cpp.DataObject()
print(bool(dataObj))

It will output :

False
True
True

The first one should also be True. Note how DataObject works differently from AnyDataWrapper.
One difference is that DataObject is not templated while AnyDataWrapper is.

I debugged this to find:

So this comes from unlucky interaction between two cppyy mechanisms and the signature of AnyDataWrapperBase::size().

The smoking gun is that the two objects differ in whether they have __len__ at all:

>>> hasattr(intObj, "__len__")
True
>>> hasattr(dataObj, "__len__")
False

Two pieces in ROOT's vendored CPyCppyy are relevant:

  1. Any class with a method named size gets __len__ aliased to it. No check on the return type. See here:
// for STL containers, and user classes modeled after them
if (HasAttrDirect(pyclass, PyStrings::gSize, /*mustBeCPyCppyy=*/ true)) {
    Utility::AddToClass(pyclass, "__len__", "size");
}
  1. So bool(x) on any cppyy object is: non-null pointer → consult len if there is one, otherwise True. See op_nonzero:
if (!self->GetObject())
    return 0;
PyObject* pylen = PyObject_CallMethodObjArgs((PyObject*)self, PyStrings::gLen, NULL);
if (!pylen) { PyErr_Clear(); return 1; }
return PyObject_IsTrue(pylen);

Put those together with AnyDataWrapperBase::size() in GaudiKernel/AnyDataWrapper.h, which is declared as std::optional<std::size_t> and returns std::nullopt for any non-container T (via the details::size fallback). AnyDataWrapper<int> inherits that, (1) aliases it to len, then (2) bool() goes through op_nonzero and calls it. You can see the mess directly:

>>> intObj.__len__.__doc__
'optional<unsigned long> AnyDataWrapper<int>::size()'
>>> len(intObj)
TypeError: 'optional<unsigned long>' object cannot be interpreted as an integer

DataObject has no size() at all, so no __len__ is installed, op_nonzero hits the PyErr_Clear(); return 1; branch, and you get True.

Changes or fixes:

Guard the automatic size() -> __len__ pythonization to require that size() returns an integer type and the class has either begin()/end() methods or accepts operator[]. This prevents bool() returning False for valid objects whose size() returns non-integer types like std::optional<std::size_t>.

Checklist:

  • tested changes locally
  • updated the docs (if necessary)

@chrisburr chrisburr force-pushed the fix/cppyy-size-len-pythonization branch from 5815907 to 7a2ba49 Compare April 13, 2026 14:12
@chrisburr chrisburr marked this pull request as ready for review April 13, 2026 14:13
Copy link
Copy Markdown
Contributor

@guitargeek guitargeek left a comment

Choose a reason for hiding this comment

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

Thank you very much for stepping up to fix this, that's very much appreciated!

My main request is to make the integer check a part of the backend library, and a few other small things.

@guitargeek
Copy link
Copy Markdown
Contributor

@chrisburr, how far do you need this packported? We need to cut somewhere, since the problem existed forever.

I also commited another fix related to this a few months ago, by the way:
e7d3bed

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 13, 2026

Test Results

    22 files      22 suites   3d 1h 41m 39s ⏱️
 3 833 tests  3 830 ✅  1 💤  2 ❌
75 653 runs  75 606 ✅ 18 💤 29 ❌

For more details on these failures, see this check.

Results for commit 18ad0e0.

♻️ This comment has been updated with latest results.

@chrisburr chrisburr force-pushed the fix/cppyy-size-len-pythonization branch from 7a2ba49 to 18ad0e0 Compare April 14, 2026 02:39
@chrisburr
Copy link
Copy Markdown
Contributor Author

how far do you need this packported? We need to cut somewhere, since the problem existed forever.

I don't know much about the issue this caused so @sponce is probably better positioned to answer.

@sponce
Copy link
Copy Markdown

sponce commented Apr 14, 2026

No need to backport further that LCG108b, aka ROOT 6.36.08 for us

Guard the automatic size() -> __len__ pythonization to require that
size() returns an integer type and the class has begin()/end() methods
or operator[]. This prevents bool() returning False for valid objects
whose size() returns non-integer types like std::optional<std::size_t>.

Use Cppyy::IsIntegerType (backed by Clang's QualType) to check the
return type, which correctly resolves typedefs like size_type. Walk the
MRO when checking for size/begin/end/getitem attributes, since
HasAttrDirect only checks the class's own __dict__. Skip pythonization
if size() has multiple overloads.

Update tests for stl_like_class2/3 which have incomplete container
interfaces (missing iterators or returning non-iterator types from
begin/end).
@chrisburr chrisburr force-pushed the fix/cppyy-size-len-pythonization branch from 18ad0e0 to 002caaf Compare April 14, 2026 09:48
Copy link
Copy Markdown
Contributor

@guitargeek guitargeek left a comment

Choose a reason for hiding this comment

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

Thank you very much for improving the heuristics for the size()-to-__len__ Pythonization and addressing the review comments!

@guitargeek guitargeek merged commit cccbacc into root-project:master Apr 14, 2026
28 of 29 checks passed
@guitargeek
Copy link
Copy Markdown
Contributor

Fix was backported to 6.36 and 6.38

@chrisburr chrisburr deleted the fix/cppyy-size-len-pythonization branch April 14, 2026 12:15
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.

3 participants