Skip to content

Conversation

@seberg
Copy link
Member

@seberg seberg commented Nov 8, 2025

This is a lot more complex then I expected. We, correctly, deprecated .dtype attribute lookup recursion, however...
The code still had a try/except that still recursed and that try except carried meaning at least in a weird path of:

class mydtype(np.void):
    dtype = ...

Now does that make sense? Maybe not... we also fall back to object in some paths which should have been broken when a dtype attribute existed on a type but it is a property/descriptor.

So, this removes the recursion, but adds a check for __get__ to filter out those cases, this is something we successfully did for other protocols __array__, __array_interface__, etc.

This is a lot more complex then I expected. We, correctly, deprecated
`.dtype` attribute lookup recursion, however...
The code still had a `try/except` that still recursed and that try
except carried meaning at least in a weird path of:
```
class mydtype(np.void):
    dtype = ...
```

Now does that make sense? Maybe not... we also fall back to object
in some paths which should have been broken when a dtype attribute
existed on a type but it is a property/descriptor.

So, this removes the recursion, but adds a check for `__get__` to
filter out those cases, this is something we successfully did for
other protocols `__array__`, `__array_interface__`, etc.

Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>
@seberg seberg added 03 - Maintenance 30 - API and removed 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes labels Nov 8, 2025
@mattip
Copy link
Member

mattip commented Nov 8, 2025

Would adding a __numpy_dtype__ attribute to np.float64 make the detection logic simpler? The documentation mentions one must use np.dtype(np.float64), maybe we could have np.float64.__numpy_dytpe__ instead

@seberg
Copy link
Member Author

seberg commented Nov 9, 2025

Yeah, that is a nice thought, we should maybe add it to deprecate the use of the dtype on the instance.
I think there are two things that make me not jump at it. First, we need type lookup for some things anyway (i.e. I would prefer not to require this for user DTypes; that said such user dtypes don'tsupport the conversion right nowI suspect, that should be fixed).
Second, it does need a careful property dance, since the class attribute needs to give a dtype, while the instance attribute should give an error. That said... while I sm not sure that the Python API makes that simple, it alsodoesn't make it hard.

I guess that makesme lean to: worth an issue/followup, but maybe not needed here?

Comment on lines 10 to 12
For array-like objects we encourage you to implement ``__numpy_dtype__``
with a warning or error to _prevent_ using e.g. ``dtype=dataframe`` in
NumPy functions (it may be good to go via a Deprecation).
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need other array libraries to take action? If they don't do anything, the status-quo for these types of array-likes is maintained (works or doesn't work depending on the case).

A warning / deprecation / error can be handled centrally in numpy. You could even make this behavior dependent on whether the passed object is array-like and/or whether they return a numpy dtype.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let me rephrase it to say that you could do this. NumPy would indeed deprecate it for you as soon as we actually deprecate .dtype.
I agree with that deprecation, but think we may want to wait a little bit, implementing it in the downstream library would be an early opt-in effectively.

@seberg seberg added this to the 2.4.0 release milestone Nov 19, 2025
Copy link
Contributor

@inakleinbottle inakleinbottle left a comment

Choose a reason for hiding this comment

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

I had a look through the changes to the C code. I can't see any obvious issues in the C code.

dt_instance = dt()
dt_instance.dtype = dt
with pytest.raises(RecursionError):
with pytest.raises(ValueError):
Copy link
Member

Choose a reason for hiding this comment

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

Does this behavior change deserve a release note?

Copy link
Member Author

Choose a reason for hiding this comment

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

🤷 I don't think recursion errors are a type of error that anyonen should ever run into really.

Copy link
Member

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

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

LGTM, just one small comment about whether the changing error type deserves a release note

@charris charris merged commit e16fc01 into numpy:main Nov 27, 2025
91 of 96 checks passed
@charris
Copy link
Member

charris commented Nov 27, 2025

Thanks Sebastian.

@jorenham
Copy link
Member

I guess I'll do the typing stuff for this tomorrow then (in 12 hours or so). I assume that branching 2.4 is still planned for his weekend?

@seberg seberg deleted the __numpy_dtype__ branch November 28, 2025 07:57
@jorenham
Copy link
Member

jorenham commented Nov 28, 2025

Maybe we could add a __numpy_dtype__ to dtype itself, i.e.e.g. dtype(int8).__numpy_dtype__ == dtype(int8)?

And how about also adding it to the scalar-type classes, e.g.i.e. int8.__numpy_dtype__ == dtype(int8)?

That way we'd be able to simplify numpy._typing._SupportsDType and numpy.typing.DTypeLike, from

# numpy._typing
class _SupportsDType[DTypeT: np.dtype = np.dtype](Protocol):
    @property
    def dtype(self) -> DTypeT: ...

type _VoidDTypeLike = # <omitted spaghetti>
type _DTypeLike[ScalarT: np.generic] = type[ScalarT] | np.dtype[ScalarT] | _SupportsDType[np.dtype[ScalarT]]

# numpy.typing
type DTypeLike =  np.dtype | type | str | _SupportsDType | _VoidDTypeLike

to just

# numpy._typing
class _SupportsDType[DTypeT: np.dtype = np.dtype](Protocol):
    @property
    def __numpy_dtype__(self) -> DTypeT: ...

type _VoidDTypeLike = # <omitted spaghetti>
type _DTypeLike[ScalarT: np.generic] = _SupportsDType[np.dtype[ScalarT]]

# numpy.typing
type DTypeLike = type | str | _DTypeLike | _VoidDTypeLike

(this uses Python 3.13+ PEP 696 syntax for aesthetics)

Typing aside, I suppose it'd also be nice if we'd "practice what we preach", analogous to e.g. ndarray.__array__, int.__int__, str.__str__, etc.

@seberg
Copy link
Member Author

seberg commented Nov 28, 2025

And how about also adding it to the scalar-type classes

Possible, but is it worth it? You need to make a funky dance with a custom descriptor stuffed into the __dict__ again, because you must error when accessing it from the class...

I do also do like to (in principle!) allow tying scalar types to dtypes that do not allow adding a custom attribute like this (becuase they are defined by a 3rd party). I.e. that still needs something like the sctypeDict "registration".

EDIT: Although, I guess typing wise the last thing might not matter too much, in the sense that I am not sure user dtypes are supported typing wise to begin with...

@charris
Copy link
Member

charris commented Nov 28, 2025

I assume that branching 2.4 is still planned for his weekend?

Only if we are ready.

@jorenham
Copy link
Member

I.e. that still needs something like the sctypeDict "registration".

EDIT: Although, I guess typing wise the last thing might not matter too much, in the sense that I am not sure user dtypes are supported typing wise to begin with...

Yea, that's right. It'd be similar to that ABCMeta.register stuff, that type-checkers also aren't able to deal with. I'm not even sure if there exists a type theory (yes; there's not just one "type-theory") that would allow for such a thing, considering the (arguably) most important static typing axiom, "types don't change" (at runtime), that all type theories share (as far as I'm aware of).

The approach that we currently use to statically determine a scalar type's associated dtype, via its .dtype attribute (with a typing.Protocol). But since it's an instance attribute, and the dtype constructor only accepts scalar classes, that can be pretty awkward to deal with.

@jorenham
Copy link
Member

jorenham commented Nov 28, 2025

Possible, but is it worth it? You need to make a funky dance with a custom descriptor stuffed into the __dict__ again, because you must error when accessing it from the class...

Can't we use _DTypeMeta for that?

@seberg
Copy link
Member Author

seberg commented Nov 28, 2025

You are confusing scalar types and dtypes.

@jorenham
Copy link
Member

You are confusing scalar types and dtypes.

Ah yea, you're right; classic mistake :P

cakedev0 pushed a commit to cakedev0/numpy that referenced this pull request Dec 5, 2025
numpy#30179)

* MAINT,API: Introduce __numpy_dtype__ and fix dtype attribute recursion

This is a lot more complex then I expected. We, correctly, deprecated
`.dtype` attribute lookup recursion, however...
The code still had a `try/except` that still recursed and that try
except carried meaning at least in a weird path of:
```
class mydtype(np.void):
    dtype = ...
```

Now does that make sense? Maybe not... we also fall back to object
in some paths which should have been broken when a dtype attribute
existed on a type but it is a property/descriptor.

So, this removes the recursion, but adds a check for `__get__` to
filter out those cases, this is something we successfully did for
other protocols `__array__`, `__array_interface__`, etc.

Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>

* DOC: Add release note snippet

* DOC: Make release note more precise, array-likes don't need the new attr

---------

Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>
IndifferentArea pushed a commit to IndifferentArea/numpy that referenced this pull request Dec 7, 2025
numpy#30179)

* MAINT,API: Introduce __numpy_dtype__ and fix dtype attribute recursion

This is a lot more complex then I expected. We, correctly, deprecated
`.dtype` attribute lookup recursion, however...
The code still had a `try/except` that still recursed and that try
except carried meaning at least in a weird path of:
```
class mydtype(np.void):
    dtype = ...
```

Now does that make sense? Maybe not... we also fall back to object
in some paths which should have been broken when a dtype attribute
existed on a type but it is a property/descriptor.

So, this removes the recursion, but adds a check for `__get__` to
filter out those cases, this is something we successfully did for
other protocols `__array__`, `__array_interface__`, etc.

Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>

* DOC: Add release note snippet

* DOC: Make release note more precise, array-likes don't need the new attr

---------

Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>
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.

7 participants