Skip to content

ENH: Allow subscript access for np.bool by adding __class_getitem__ #29370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

riku-sakamoto
Copy link
Contributor

@riku-sakamoto riku-sakamoto commented Jul 13, 2025

Fixes #29247

This PR adds support for subscript access to np.bool by implementing a __class_getitem__ method.

Implementation Details

  • Implements booleantype_class_getitem_abc, following the pattern used for other scalar types.
  • Adds __class_getitem__ to the np.bool type.

@ngoldbaum

I created new PR.
Would you mind reviewing this when you get a chance? Thank you!

@riku-sakamoto riku-sakamoto marked this pull request as ready for review July 13, 2025 20:20
cls[Any]
if cls == np.bool:
# np.bool allows subscript
assert cls[Any]
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this one has been sitting for a month or so, perhaps because it is a bit esoteric. I checked locally that the regression test fails before the source patch is applied.

I also verified that assert_type(cls, np.dtype[npt.NDArray[bool]]) works on this branch, as does the original assert_type(cls, np.dtype[np.bool[bool]]). I find the latter a little harder to parse, perhaps because it appears to be information-redundant, but I mostly gave up on understanding the static typing details beyond the surface level.

Anyway, I guess this one is for @jorenham to check? Would it make sense to have the explicit assert_type check somewhere as well, rather than just the absence of error here? I'm pretty sure I've seen some test files with assert_type all over the place--not sure if those would run the runtime checks or just the "type checking" time checks, but Joren will know.

I'm not sure I really want to know the answer, but is there an intuitive explanation for why only the np.bool type allows subscripting, and not the others?

The runtime type check seems a bit confusing though, because the diff below the fold also appears to pass on this branch:

--- a/numpy/_core/tests/test_scalar_methods.py
+++ b/numpy/_core/tests/test_scalar_methods.py
@@ -4,7 +4,7 @@
 import fractions
 import platform
 import types
-from typing import Any
+from typing import Any, assert_type
 
 import pytest
 
@@ -171,12 +171,7 @@ def test_abc_non_numeric(self, cls: type[np.generic]) -> None:
     @pytest.mark.parametrize("code", np.typecodes["All"])
     def test_concrete(self, code: str) -> None:
         cls = np.dtype(code).type
-        if cls == np.bool:
-            # np.bool allows subscript
-            assert cls[Any]
-        else:
-            with pytest.raises(TypeError):
-                cls[Any]
+        assert_type(cls, np.dtype[np.bool[bool]])

The reveal_type seems pretty crude for cls in general across the types at runtime here, which kind of makes me wonder how much value one really gets out of the runtime check in the original ticket--I suppose not much, and the only real argument was that it shouldn't error out.

Copy link
Contributor Author

@riku-sakamoto riku-sakamoto Aug 16, 2025

Choose a reason for hiding this comment

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

Thank you for checking this so thoroughly!

Would it make sense to have the explicit assert_type check somewhere as well, rather than just the absence of error here?

I agree it could be useful to add an explicit assert_type test.
I am happy to update the PR to include that. But, I am also not sure about the right place to add them, so I will wait an answer from Joren.

is there an intuitive explanation for why only the np.bool type allows subscripting, and not the others?

If my understanding is correct, some users perfer to treat True and False as independent types similar to Literal[True] and Literal[False]. (Boolean has only two variants, so it is possible to do so.)

def sample(must_positive_flags: np.bool[True]):
   ....

The reveal_type seems pretty crude for cls in general across the types at runtime here, which kind of makes me wonder how much value one really gets out of the runtime check in the original ticket--I suppose not much, and the only real argument was that it shouldn't error out.

Thank you again for the further suggestions. I didn't realize that reveal_type is so crude for cls.
I might have over-modified things here, as you point out. I'll fix it if you think it’s preferable.

Copy link
Member

Choose a reason for hiding this comment

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

I also verified that assert_type(cls, np.dtype[npt.NDArray[bool]]) works on this branch, as does the original assert_type(cls, np.dtype[np.bool[bool]]). I find the latter a little harder to parse, perhaps because it appears to be information-redundant, but I mostly gave up on understanding the static typing details beyond the surface level.

You could also write assert_type(cls, np.dtype[np.bool]), because np.bool is exactly equivalent to np.bool[bool].

Would it make sense to have the explicit assert_type check somewhere as well, rather than just the absence of error here? I'm pretty sure I've seen some test files with assert_type all over the place--not sure if those would run the runtime checks or just the "type checking" time checks, but Joren will know.

assert_type is purely a static check, and this PR only affects runtime behavior, so I don't think that's needed.

I'm not sure I really want to know the answer, but is there an intuitive explanation for why only the np.bool type allows subscripting, and not the others?

As @riku-sakamoto correctly explained, the main use-case of np.bool's generic type parameter, is so that static type-checkers are able to distinguish between np.True_ and np.False_.
In scipy-stubs, for example, this allows users to pass e.g. keepdims=np.True_, and the return type will be inferred as e.g. Array[float64]. If np.bool wouldn't have been generic, then that would instead have been float64 | Array[float64].

But the downside of having this generic type-parameter, is that type-checkers will now display the type of a: np.bool = ... as np.bool[bool]. That's because the np.bool type parameter defaults to builtins.bool if omitted. Type-checkers like pyright and mypy will interpret np.bool as np.bool[builtins.bool], and report it as such.
But they don't have to display it that way. The type-checkers could also have decided to report np.bool as np.bool. So the way I see it, this unnecessarily verbose formatting is not our responsibility, but that of the type-checkers.

Copy link
Member

Choose a reason for hiding this comment

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

Another use-case that's kinda fun (and kinda niche), is that static type-checkers now understand boolean algebra:

assert_type(b0_ | b0_, np.bool[FalseType])
assert_type(b0_ | b1_, np.bool[TrueType])
assert_type(b1_ | b0_, np.bool[TrueType])
assert_type(b1_ | b1_, np.bool[TrueType])
assert_type(b0_ ^ b0_, np.bool[FalseType])
assert_type(b0_ ^ b1_, np.bool[TrueType])
assert_type(b1_ ^ b0_, np.bool[TrueType])
assert_type(b1_ ^ b1_, np.bool[FalseType])
assert_type(b0_ & b0_, np.bool[FalseType])
assert_type(b0_ & b1_, np.bool[FalseType])
assert_type(b1_ & b0_, np.bool[FalseType])
assert_type(b1_ & b1_, np.bool[TrueType])

where b0_ = np.False_ and b1_ = np.True_.

Copy link
Member

Choose a reason for hiding this comment

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

def sample(must_positive_flags: np.bool[True]):
   ....

nitpick: you forgot the Literal (and there are 4 dots), i.e.

def sample(must_positive_flags: np.bool[Literal[True]]): ...

Fixes numpy#29247. Implements `booleantype_class_getitem_abc` and adds a `__class_getitem__` method to `np.bool` to enable subscript access (e.g., `np.bool[int]`).
@riku-sakamoto riku-sakamoto force-pushed the add_boolean_type_subscribe branch from 8b04fc2 to 0dbbed6 Compare August 17, 2025 05:46
@riku-sakamoto riku-sakamoto force-pushed the add_boolean_type_subscribe branch from 0dbbed6 to 003281b Compare August 17, 2025 05:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BUG: np.bool is not subscriptable at run time
3 participants