beartype
beartype copied to clipboard
[Docos] New FAQ entry on "Why are type-checking violation messages so unreadable, long, bad, and awful?"
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?"
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
-
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. -
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.
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."