Skip to content

restricting dynamic attribute assignment for object backend using __slots__#700

Draft
Schefflera-Arboricola wants to merge 1 commit into
scikit-hep:mainfrom
Schefflera-Arboricola:adding_slots
Draft

restricting dynamic attribute assignment for object backend using __slots__#700
Schefflera-Arboricola wants to merge 1 commit into
scikit-hep:mainfrom
Schefflera-Arboricola:adding_slots

Conversation

@Schefflera-Arboricola
Copy link
Copy Markdown
Contributor

@Schefflera-Arboricola Schefflera-Arboricola commented Apr 27, 2026

Description

Kindly take a look at CONTRIBUTING.md.

Please describe the purpose of this pull request. Reference and link to any relevant issues or pull requests.

I was experimenting with the Vector library and saw that we can assign z to a VectorObject2D object and it neither throws an error nor does it automatically change it to a VectorObject3D:

import vector
vec=vector.obj(x=1, y=2)
vec.z=8    # no error
print(type(vec))    
# output: <class 'vector.backends.object.VectorObject2D'>
vec.blah_blah=78    # no error

I saw there were some conversations about adding __slots__ here.. so i started working on this.

The VectorObject2/3/4D classes already had __slots__ correctly defined but they were not being used because their parent classes had no __slots__ defined so the default __dict__ was still getting created (refer https://docs.python.org/3/reference/datamodel.html#slots )-- so it was allowing adding any attribute to a VectorObject2D object, like vec.blah_blah=78.

In this PR I added __slots__ = () to parent classes and now the above code raises an error:

>>> import vector
>>> vec=vector.obj(x=1, y=2)
>>> vec.z=8
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'VectorObject2D' object has no attribute 'z'

But, because the classes in _methods.py are also being used by the Awkward arrays backend-- so there were some test failures (test logs under this PR)

error summary
======================================================================================== short test summary info =========================================================================================
FAILED tests/backends/test_awkward.py::test_dimension_conversion[cpu] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_dimension_conversion[typetracer] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_basic[cpu] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_basic[typetracer] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_rotateZ[cpu] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_rotateZ[typetracer] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_like[cpu] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_like[typetracer] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_momentum_coordinate_transforms[cpu] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_momentum_coordinate_transforms[typetracer] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_momentum_preservation[cpu] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_momentum_preservation[typetracer] - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_subclass_fields[cpu] - TypeError: __class__ assignment: 'LorentzVectorArray' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_subclass_fields[typetracer] - TypeError: __class__ assignment: 'LorentzVectorArray' object layout differs from 'Array'
FAILED tests/backends/test_awkward_numba.py::test - TypeError: __class__ assignment: 'MomentumArray4D' object layout differs from 'Array'
FAILED tests/backends/test_dask_awkward.py::test_necessary_columns - TypeError: __class__ assignment: 'MomentumArray2D' object layout differs from 'Array'
FAILED tests/test_issues.py::test_issue_161 - TypeError: __class__ assignment: 'MomentumArray4D' object layout differs from 'Array'
FAILED tests/test_issues.py::test_issue_443 - TypeError: __class__ assignment: 'MomentumArray4D' object layout differs from 'Array'
FAILED tests/test_issues.py::test_issue_621 - TypeError: __class__ assignment: 'MomentumArray4D' object layout differs from 'Array'
FAILED tests/test_notebooks.py::test_awkward - papermill.exceptions.PapermillExecutionError: 
FAILED tests/test_notebooks.py::test_numba - papermill.exceptions.PapermillExecutionError: 
=============================================================================== 21 failed, 805 passed in 107.23s (0:01:47) ===============================================================================

Right now, I'm not very familiar with how the awkward arrays are integrated within Vector. I would appreciate any guidance on what's the best way to make this work without bothering the Awkward arrays backend (@henryiii @jpivarski ). Pls LMK if that's even possible without creating separate _methods.pys for every backend, if not then feel free to close this PR.

Thank you!


PS: one work-around I tried was to put ak.Array first in the class definition for classes MomentumArray2/3/4D i.e. class MomentumArray4D(ak.Array, MomentumAwkward4D): instead of class MomentumArray4D(MomentumAwkward4D, ak.Array):. Although it made some tests pass, I don't think it's a good solution bcoz it might break some user code, as it would give ak.Array's methods priority over MomentumAwkward2/3/4D's methods when there will be any conflicts; and the test_subclass_fields was still failing.

error logs
____________________________________________________________________________________ test_subclass_fields[typetracer] ____________________________________________________________________________________

backend = 'typetracer'

    @pytest.mark.parametrize("backend", ["cpu", "typetracer"])
    def test_subclass_fields(backend):
        @ak.mixin_class(vector.backends.awkward.behavior)
        class TwoVector(vector.backends.awkward.MomentumAwkward2D):
            pass
    
        @ak.mixin_class(vector.backends.awkward.behavior)
        class ThreeVector(vector.backends.awkward.MomentumAwkward3D):
            pass
    
        @ak.mixin_class(vector.backends.awkward.behavior)
        class LorentzVector(vector.backends.awkward.MomentumAwkward4D):
            @ak.mixin_class_method(np.divide, {numbers.Number})
            def divide(self, factor):
                return self.scale(1 / factor)
    
        LorentzVectorArray.ProjectionClass2D = TwoVectorArray  # noqa: F821
        LorentzVectorArray.ProjectionClass3D = ThreeVectorArray  # noqa: F821
        LorentzVectorArray.ProjectionClass4D = LorentzVectorArray  # noqa: F821
        LorentzVectorArray.MomentumClass = LorentzVectorArray  # noqa: F821
    
        vec = ak.to_backend(
>           ak.zip(
                {
                    "pt": [[1, 2], [], [3], [4]],
                    "eta": [[1.2, 1.4], [], [1.6], [3.4]],
                    "phi": [[0.3, 0.4], [], [0.5], [0.6]],
                    "energy": [[50, 51], [], [52], [60]],
                },
                with_name="LorentzVector",
                behavior=vector.backends.awkward.behavior,
            ),
            backend=backend,
        )

tests/backends/test_awkward.py:1157: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/_dispatch.py:41: in dispatch
    with OperationErrorContext(name, args, kwargs):
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/_errors.py:80: in __exit__
    raise self.decorate_exception(exception_type, exception_value)
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/_dispatch.py:67: in dispatch
    next(gen_or_result)
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/operations/ak_zip.py:153: in zip
    return _impl(
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/operations/ak_zip.py:265: in _impl
    wrapped_out = ctx.wrap(out, highlevel=highlevel)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/_layout.py:181: in wrap
    return wrap_layout(
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/_layout.py:234: in wrap_layout
    return awkward.highlevel.Array(content, behavior=behavior, attrs=attrs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/highlevel.py:365: in __init__
    self._update_class()
../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/highlevel.py:382: in _update_class
    self.__class__ = get_array_class(self._layout, self._behavior)
    ^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[TypeError("__class__ assignment: 'LorentzVectorArray' object layout differs from 'Array'") raised in repr()] Array object at 0x10e7e1940>, name = '__class__'
value = <class 'tests.backends.test_awkward.LorentzVectorArray'>

    def __setattr__(self, name, value):
        """
        Args:
            where (str): Attribute name to set
    
        Set an attribute on the array.
    
        Only existing public attributes e.g. #ak.Array.layout, or private
        attributes (with leading underscores), can be set.
    
        Fields are not assignable to as attributes, i.e. the following doesn't work:
    
            array.z = new_field
    
        Instead, always use #ak.Array.__setitem__:
    
            array["z"] = new_field
    
        or #ak.with_field:
    
            array = ak.with_field(array, new_field, "z")
    
        to add or modify a field.
        """
        if name.startswith("_") or hasattr(type(self), name):
>           super().__setattr__(name, value)
E           TypeError: __class__ assignment: 'LorentzVectorArray' object layout differs from 'Array'
E           
E           This error occurred while calling
E           
E               ak.zip(
E                   {'pt': [[1, 2], [], [3], [4]], 'eta': [[1.2, 1.4], [], [1.6], [3.4]],...
E                   with_name = 'LorentzVector'
E                   behavior = {('*', 'Vector2D'): <class 'vector.backends.awkward.Vector...
E               )

../../.pyenv/versions/3.12.13/lib/python3.12/site-packages/awkward/highlevel.py:1327: TypeError

======================================================================================== short test summary info =========================================================================================
FAILED tests/backends/test_awkward.py::test_subclass_fields[cpu] - TypeError: __class__ assignment: 'LorentzVectorArray' object layout differs from 'Array'
FAILED tests/backends/test_awkward.py::test_subclass_fields[typetracer] - TypeError: __class__ assignment: 'LorentzVectorArray' object layout differs from 'Array'
FAILED tests/test_notebooks.py::test_awkward - papermill.exceptions.PapermillExecutionError: 
FAILED tests/test_notebooks.py::test_numba - papermill.exceptions.PapermillExecutionError: 
================================================================================ 4 failed, 822 passed in 97.58s (0:01:37) ================================================================================

Checklist

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't any other open Pull Requests for the required change?
  • Does your submission pass pre-commit? ($ pre-commit run --all-files or $ nox -s lint)
  • Does your submission pass tests? ($ pytest or $ nox -s tests)
  • Does the documentation build with your changes? ($ cd docs; make clean; make html or $ nox -s docs)
  • Does your submission pass the doctests? ($ pytest --doctest-plus src/vector/ or $ nox -s doctests)

Before Merging

  • Summarize the commit messages into a brief review of the Pull request.

@Schefflera-Arboricola Schefflera-Arboricola marked this pull request as draft April 27, 2026 16:57
@Saransh-cpp Saransh-cpp self-requested a review April 27, 2026 20:16
Copy link
Copy Markdown
Member

@Saransh-cpp Saransh-cpp left a comment

Choose a reason for hiding this comment

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

Thanks for working on this, @Schefflera-Arboricola!

creating separate _methods.pys for every backend

We definitely don't want this.

I don't think it's a good solution bcoz it might break some user code

Yes, that does not sound ideal either :(

I'll look into this soon-ish.

@Saransh-cpp Saransh-cpp self-requested a review April 27, 2026 20:20
@pfackeldey
Copy link
Copy Markdown
Collaborator

Awkward arrays already prohibit this kind of attribute assignment through https://github.com/scikit-hep/awkward/blob/main/src/awkward/highlevel.py#L1302-L1335, so awkward arrays of vectors does not have this issue that you can assign attributes dynamically, see:

import awkward as ak
import vector

vector.register_awkward()

arr = ak.Array(
  [
    [{"x": 1.1, "y": 2.2}, {"x": 3.3, "y": 4.4}],
    [],
    [{"x": 5.5, "y": 6.6}],
  ],
  with_name="Vector2D",
  behavior=vector.backends.awkward.behavior,
)

arr.z = 1
>>> ... AttributeError: only private attributes (started with an underscore) can be set on arrays

Vector objects and NumPy arrays of vectors however allow this. For NumPy arrays of vectors I think you'd have to overwrite one of the dtype fields to mess up the vector values (I forgot if that can be done through setattr). Vector objects seem to be mutable and you can "mess up" the vector, e.g.:

obj = vector.obj(x=1.1, y=2.2)
print(obj.rho)
>>> np.float64(2.459674775249769)

obj.x = 2.0
print(obj.rho)
>>> np.float64(2.973213749463701)

Which I am not sure if we want to support this mutability? @Saransh-cpp is there any potential Numba reason why we would want to allow this? If not, I think the safest way would be to disallow setattr and make vector objects immutable by adjusting the VectorObject, e.g. we could add a custom __setattr__ implementation there, __slots__ also works but may be more complicated to propagate correctly to all subclasses - not sure, I haven't looked at the class hierarchy too closely.

@Saransh-cpp
Copy link
Copy Markdown
Member

Making them immutable sounds good, @pfackeldey. See #158 and #457 for more discussions on this.

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