Skip to content
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

[Docos] New FAQ entry on "Why are type-checking violation messages so unreadable, long, bad, and awful?" #311

Open
leycec opened this issue Nov 30, 2023 · 2 comments

Comments

@leycec
Copy link
Member

leycec commented Nov 30, 2023

Quebec data science superstart @felixchenier just discovered a hitherto unacknowledged workaround for generating human-readable type-checking violation messages from otherwise unreadable type hints like NumPy's own numpy.typing.ArrayLike. ArrayLike is a really non-trivial union of, like, a billion different low-level private types and type hints scattered throughout the NumPy codebase. Type-checking violation messages involving ArrayLike necessarily devolve into a barren scrum of terminal gore and eye-gouging ASCII.

Take it away, Felix-bro!

Using numpy.typing.ArrayLike was not generating happy messages, e.g.

Method kineticstoolkit.timeseries.TimeSeries.__init__() parameter time={}
was expected to be of type typing.Union[numpy._typing._array_like.
_SupportsArray[numpy.dtype[typing.Any]],numpy._typing.
_nested_sequence._NestedSequence[numpy._typing._array_like.
_SupportsArray[numpy.dtype[typing.Any]]], bool, int, float, complex, str, bytes, 
numpy._typing._nested_sequence._NestedSequence[typing.Union[
bool, int, float, complex, str, bytes]]]

Not what I call a clean output.

So I cheated and did this in a custom private typing_.py module instead:

from typing import NewType, TYPE_CHECKING
from numpy.typing import ArrayLike as npt_ArrayLike

# Define custom types so that beartype, sphinx,
# mypy and the user are all happy
if TYPE_CHECKING:  # mypy is running
    ArrayLike = npt_ArrayLike
else:  # runtime
    ArrayLike = NewType("ArrayLike", npt_ArrayLike)  # mypy cries

Now, mypy is still happy, and the same error as above prints like:

TypeError: Method kineticstoolkit.timeseries.TimeSeries.__init__() parameter time={}
was expected to be of type kineticstoolkit.typing_.ArrayLike.

which I believe is much more helpful.

4D Chess Moves R Us

OMG. That solution is insanely clever. I'd actually never thought of abusing typing.NewType to generate human-readable violation messages from otherwise unreadable type hints like numpy.typing.ArrayLike. Neither has anyone else, frankly. You're definitely the first to discover an actual use case for typing.NewType. In fact, I'd written typing.NewType off as utterly useless for everything.

4D chess move right there. Seriously. I'm dead serious here. Let's publicly document this for everyone with a new official FAQ entry appropriately entitled:

"Why are type-checking violation messages so unreadable, long, bad, and awful?"

@felixchenier
Copy link
Contributor

felixchenier commented Nov 30, 2023

Hi @leycec

I think I may put a last nail in this coffin, by proposing a last modification to the hint_overrides option.

First, two tiny problems

  1. Using a little more research, I seemed to understand that NewType is used only by static type checkers and its role is to define subclasses of given types. This is why NewType(numpy.array.ArrayLike) is not appreciated by mypy, it's because ArrayLike is not a class but a large union instead. We cannot subclass this. And the reason why it works at runtime is that NewType does simply nothing at runtime. I think this is smelly, because it uses a function "badly" and in a bad context.

  2. Using the proposed workaround, we get a message that refers to a module that's used only as a workaround (in this case, packagename.typing_) and that has no value but confusion to the end user.

TypeError: Method kineticstoolkit.timeseries.TimeSeries.__init__() parameter time={}
was expected to be of type kineticstoolkit.typing_.ArrayLike.

Now, one big final solution

We just added an option hint_overrides that's thankfully not released yet and that we can still change for the best. What would you say if we added another use for it. Currently, it says:

Bear, when I say "ThisType", compare to "ThisTypeInstead"

It could as well say:

Bear, when I way "ThisType", compare to "ThisTypeInstead", and publicly use "ThisNameInstead"

This could be as simple as using (name, type) tuples in the BeartypeHintOverrides values:

The use case (warning, it's very shiny and could hurt your eyes)

from typing import NewType, TYPE_CHECKING
from numpy.typing import ArrayLike as npt_ArrayLike
from beartype import (
    beartype,
    BeartypeConf,
    BeartypeHintOverrides,
)

if TYPE_CHECKING:
    # mypy is running. ArrayLike = ArrayLike
    ArrayLike = npt_ArrayLike
else:
    # Define a dummy type for now
    # We will explicitely tell beartype how to handle this dummy type
    ArrayLike = NewType("ArrayLike", None)

typecheck = beartype(
    conf=BeartypeConf(
        hint_overrides=BeartypeHintOverrides(
            {ArrayLike: ("ArrayLike", npt_ArrayLike)}
        ),
    )
)

Now, mypy is still happy with its long ArrayLike Union, beartype is told exactly what to do with packagename.typing_.ArrayLike (check as if it was a numpy.typing.ArrayLike, but use simple name "ArrayLike" in error message) and if the end user gets a type error relative to an ArrayLike, it simply refers to "ArrayLike" which is the most helpful message. Bonus, NewType is used in an obviously supported (although surprising) way.

The question

Do you want me to implement this hint_overrides tuple configuration? I know exactly what to do.

@leycec
Copy link
Member Author

leycec commented Jan 10, 2024

Note to future self: "See this comment at #216 for a fully working example abusing using typing.NewType, typing.TypeAlias, and type aliases. I invoke thee, madness!"

Note to past self: "Invest in everything by this guy named Sam Altman."

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

No branches or pull requests

2 participants