Skip to content

Cumbersome type checking of bins property #3687

Description

@jl-wynen

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.,

x.bins.concat()

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)

  1. Add is_binned.
  2. Raise a warning in bins if the object is not binned and return None.
  3. 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!

Activity

  1. changed the title [-]CUmbersome type checking of bins property[/-] [+]Cumbersome type checking of bins property[/+] on Apr 23, 2025
  2. jokasimr commented on Apr 23, 2025

    @jokasimr
    Contributor

    I think (2) a is bit problematic because that makes it look like (non-binned) Variable has a .bins attribute, but if you try to access it that will raise an error. It's similar to today, but now at least the type checker will warn you that it might be an error to access .bins.

    An alternative, similar to (1), is to make BinnedVariable be a separate type from Variable.

  3. SimonHeybrock commented on Sep 18, 2025

    @SimonHeybrock
    Member

    I think we should go with option (2), raising and adding is_binned. We should probably:

    1. Add and release is_binned. Deprecate bins is None (only possible in the docs, unfortunately).
    2. Update downstream packages where possible.
    3. Change bins to raise.

    Unless someone has a better suggestion for a rollout?

  4. nvaytet commented on Sep 18, 2025

    @nvaytet
    Member

    Would is_binned just try to access the .bins and catch the exception if raised?

  5. self-assigned this
    on Oct 15, 2025
  6. jl-wynen commented on Nov 26, 2025

    @jl-wynen
    MemberAuthor

    Closing as the typing problem as such is now fixed. See the linked issues immediately above for the next steps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions