This is similar to #3688 but for bins.
Context
We annotate the bins property as
def bins(self) -> Bins[Self] | None:
and use x.bins is None to check whether an array contains binned data.
The problem
This is convenient at runtime but causes errors in mypy nearly every time, bins is used. E.g.,
produces
error: Item "None" of "Bins[DataArray] | None" has no attribute "concat" [union-attr]
When writing well-engineered code, this can be fine. We should check whether an array is binned and raise an appropriate error if not. But often, we know for sure that an array is binned, e.g., in chains like x.bin(...).bins.concat(...). And in user code, we would not want to constantly check for x.bins is not None. So a lot of code requires explicit casts or # type: ignore comments to pass type checks.
Note that there is a best practice recommendation to avoid union return types: https://typing.python.org/en/latest/reference/best_practices.html#arguments-and-return-types
Potential solutions
(1) Make Bins a type parameter
Something along the lines of
B = TypeVar('B', Bins[Variable], None)
class Variable(Generic[B]):
@property
def bins(self) -> B:
This could be quite complicated because now all code has to use type vars in annotations. But it would allow for more explicit function annotations. And it would allow us to detect more problems statically.
If we want to go this way, we should probably also think about making the dtype, sizes, and unit part of the type signature, similarly to numpy. (See also #3688)
(2) Raise runtime error if not binned
Instead of returning None when an array is not binned, we could raise an error. That error would be clearer than the current
AttributeError: 'NoneType' object has no attribute 'concat'
we would get from using .bins erroneously. And it would mean that simple code like x.bins.concat() passes type checks. We would then need to add
def is_binned(self) -> bool:
The problem is that we have a bunch of code that relies on the current behaviour. We can ease the transition by using the usual deprecation cycle: (each step is a separate release)
- Add
is_binned.
- Raise a warning in
bins if the object is not binned and return None.
- Raise an error in
bins if the object is not binned.
This is a fairly long and expensive process. I think it is worth the effort in the long run because it will lead to simpler code. But I would like to hear other opinions!
This is similar to #3688 but for bins.
Context
We annotate the
binsproperty asand use
x.bins is Noneto check whether an array contains binned data.The problem
This is convenient at runtime but causes errors in mypy nearly every time,
binsis used. E.g.,produces
When writing well-engineered code, this can be fine. We should check whether an array is binned and raise an appropriate error if not. But often, we know for sure that an array is binned, e.g., in chains like
x.bin(...).bins.concat(...). And in user code, we would not want to constantly check forx.bins is not None. So a lot of code requires explicitcasts or# type: ignorecomments to pass type checks.Note that there is a best practice recommendation to avoid union return types: https://typing.python.org/en/latest/reference/best_practices.html#arguments-and-return-types
Potential solutions
(1) Make Bins a type parameter
Something along the lines of
This could be quite complicated because now all code has to use type vars in annotations. But it would allow for more explicit function annotations. And it would allow us to detect more problems statically.
If we want to go this way, we should probably also think about making the dtype, sizes, and unit part of the type signature, similarly to numpy. (See also #3688)
(2) Raise runtime error if not binned
Instead of returning
Nonewhen an array is not binned, we could raise an error. That error would be clearer than the currentwe would get from using
.binserroneously. And it would mean that simple code likex.bins.concat()passes type checks. We would then need to addThe problem is that we have a bunch of code that relies on the current behaviour. We can ease the transition by using the usual deprecation cycle: (each step is a separate release)
is_binned.binsif the object is not binned and returnNone.binsif the object is not binned.This is a fairly long and expensive process. I think it is worth the effort in the long run because it will lead to simpler code. But I would like to hear other opinions!