PEP 646: It Probably Doesn't Do What Everyone Wants It To Do
PEP 646: It Probably Doesn't Do What Everyone Wants It To Do Hi, all! Warmest greetings on this drizzly Spring evening from the squalid Canadian wetlands, where even the mosquitoes think it's really too cold. I'm Cecil Curry (@leycec), the plucky author of beartype (https://github.com/beartype/beartype) – the pure-Python O(1) runtime type checker that pretends to do alot while actually doing not too much at all. Thus O(1). "You gets what you pays for," right? My wife and I also co-authored a several million-line pure-Python multiphysics biology simulator (https://gitlab.com/betse/betse) heavily leveraging the standard scientific stack (e.g., NumPy, SciPy, Matplotlib, NetworkX). Those were good years. I still had hair back then, so they had to have been good. I was kindly redirected here by Ryan Soklaski (@rsokl), the brilliant @beartype user I never knew I had and head instructor of the CogWorks course at the MIT Beaver Works Summer Institute. Ryan accurately predicted that PEP 646 specifically and the vigorous discussion here surrounding tensor typing in general would be right up my lakeside wheelhouse. Boy, is it! We're all over this sort of deep structural science-oriented QA. In fact, @beartype's userbase (https://github.com/beartype/beartype/stargazers) is predominated by machine learning, computer vision, and data scientists. We're poised on the awkward precipice between typing and tensors, because what everyone (including my wife and I) always wanted from @beartype was runtime type checking of scientific data structures. PEP 646 promises all this... and more! But does it deliver? Does @leycec ever regain his hair? Do Canadian brown bears groom themselves on spindly trees on our front yard? The answers to all these questions and more follow shortly. First, I'd like to profusely thank all the PEP 646 authors – Mark Mendoza, Matthew Rahtz, Pradeep Kumar Srinivasan, Vincent Siles, and Guido van Rossum – for their profuse work and dedication to the good cause. Second, I'd like to profusely apologize a priori for any untoward argumentation or firework shows I am about to generate. That's not the intention. Really! PEP 646 is currently at the steering committee stage. Nobody wants to see that derailed, because hard work deserves its just reward and GitHub karma. All that said... let's do this. The real-world motivation of PEP 646 is to convince the various authors of various popular numerical frameworks to adopt, embrace, and extend PEP 646. This is a noble effort. But can this effort actually be realized? The PEP 646 API assumes a Python-centric worldview. Specifically, PEP 646 assumes that numerical frameworks will subclass their data structures from "typing.Generic", a magical pure-Python class. Those assumptions derive from the first example in PEP 646: from typing import TypeVar, TypeVarTuple DType = TypeVar('DType') Shape = TypeVarTuple('Shape') class Array(Generic[DType, *Shape]): def __abs__(self) -> Array[DType, *Shape]: ... def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: ... But this assumption may not necessarily hold. Most popular numerical frameworks worth supporting (including numerical frameworks explicitly referenced in PEP 646, which means NumPy and TensorFlow) are implemented with at least two layers: a low-level mostly Python-agnostic layer implemented in some combination of C, C++, Fortran, or Rust referred to as the "core API" and one or more high-level layers providing bindings to high-level languages. Python is only one of many high-level languages supported by these frameworks. This includes popular numerical frameworks one might erroneously assume from their names to be Python-specific like PyTorch. Despite the name, PyTorch is also implemented as a low-level Python-agnostic C++ layer (https://pytorch.org/tutorials/advanced/cpp_frontend.html). The same goes for: * The entirety of TensorFlow's core C++ API (https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/framewo...). * *Most* but not all of NumPy's core C API (https://numpy.org/doc/stable/reference/c-api/coremath.html), which NumPy developers are desperately refactoring to be Python-agnostic. I believe the core "numpy.core._multiarray_umath.ndarray" type is still Python-specific, but that's likely to change sometime this decade. (Don't quote me on that. Please. My GitHub karma can only take so many body blows.) Is this a problem? It might very well be. Of course, it should be admitted at this crucial juncture that my idea of a fun time includes scrambling over tick-infested shrubland in an inhospitable wasteland for eight hours yesterday, which only goes to show that you can't trust anyone who voluntarily lives in a poorly insulated cottage plagued with mosquitoes. </ahem> "typing.Generic" is magical, because it defines the magical PEP 487 __init_subclass__() dunder method. Despite banging my balding head against CPython documentation and my ketchup-stained keyboard for several hours, I'm kinda unconvinced that a C[++]-based class can even subclass one or more magical or non-magical pure-Python classes. I've never seen anyone do either. Then again, I've never seen a circumpolar double rainbow on Christmas Eve composed of flying winged bugbears. Who's to say that can't happen? I'd sure like to see that someday. So let's assume for the sake of debate that's feasible – that C[++]-based classes can in fact subclass one or more magical pure-Python classes. Under that generous assumption, it's still unlikely that numerical frameworks that were either Python-agnostic from the get-go (like TensorFlow) or are rapidly refactoring their core APIs away from Python (like NumPy) would revert those fundamental decisions just to appease our small but vocal cadre of Python type-checking aficionados. (That's us: the good guys in the distinct minority.) Now I *know* what you're thinking. "Ah-ha! You poor sad Canadian hillbilly. All we need to do is strenuously encourage the authors of these numerical frameworks to define entirely new Python-specific classes in their Python bindings that comply with PEP 646 by subclassing "typing.Generic" in the exact way we want them to! It's so blindingly simple! If only you still had luxurious hair, @leycec. Then you might have thought of that. Alas. Here we are." In the dark ruminations of your monitor-lit devcave, you may even be dangerously considering unspeakable horrors like adding a new type-checking shim like this into the perfidious bowels of the NumPy codebase: from numpy.core._multiarray_umath import ndarray from typing import TypeVar, TypeVarTuple DType = TypeVar('DType') Shape = TypeVarTuple('Shape') class NDArray(ndarray, Generic[DType, *Shape]): def __abs__(self) -> Array[DType, *Shape]: return super().__abs__() def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: return super().__add__(other) Right? Something unmentionable like that, right? Of course, we can all admit to ourselves that's horrible, because that imposes one additional stack frame per method call. So, nobody will do that. Now I *know* what you're thinking. "Ah-ha! You poor sad Canadian person of indeterminate mental stability. All we need to do is define .pyi-formatted stub files instead expressing the same type semantics! If only your limbs weren't painfully arthritic, @leycec. Then you might have thought of that. Alas. Here we are." Right? Something inexpressible like that, right? Of course, we can all admit to ourselves that's horrible too, because .pyi-formatted stub files aren't sanely accessible at runtime. And we are back where we started in a cursed reenactment of "Blair Witch Project: Tensor Typing Edition." You're now thinking of either bolting type hints onto C[++]-based callables or wholly reimplementing "typing.Generic" in C, aren't you? I knew it! You are! And at this point, everyone should be thinking: "This doesn't quite seem right anymore." The core issue here is that PEP 646 requires someone who is not us to change their workflow for us. It's not necessarily clear that that will ever happen. Sure, it might. I'm an optimistic man, which means I'm not a betting man. Even if that utopic Xanadu were to become manifest, PEP 646 still seems problematic. My PEP 484 isn't strong, but it's unclear how you would: * Type tensor methods returning new tensors satisfying different constraints (especially of differing shapes). * Type tensor constraints in a forward-compatible manner. Tensor constraints declared by PEP 646 are crazy fragile. They lock any numerical framework that complies into a specific preordained ordering of tensor constraints. A tensor typed as "class Array(Generic[DType, *Shape])" can *never* be typed as anything else for all time, which is a pretty long time. Want to add another constraint like, say, PyTorch's newly introduced "layout" tensor attribute that fundamentally changes how one uses passed or returned tensors? (https://pytorch.org/docs/stable/tensor_attributes.html#torch.torch.layout) **Sorry.** No one can ever do that, because the original specification of "class Array(Generic[DType, *Shape])" prematurely locked everyone into checking only those two constraints. Surely we have sane alternatives? We do. Many! Oodles! Bucket-loads! And other colourful idioms for "lots." They all share one attribute in common: they don't require someone who is not us to change their fundamental workflow for us. The PEP 593 "typing.Annotated" class offers an illustrative example: import numpy as np from tensorflow.types.core import Tensor from typing import Annotated PixellatedRetroImage = Annotated[Tensor, np.float32, [640, 480]] def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ... That has the benefit of working now under Python >= 3.9 without necessitating that external authors, open-source communities, or for-profit corporations do unlikely (and perhaps infeasible) stuff for us. We'll be pursuing a similar solution to that in @beartype, because it works now and satisfies everyone's downstream concerns. Of course, "typing" wouldn't want to do something like that. "typing" would want to do something even more robust, extensible, and absurdly awesome like: import numpy as np from tensorflow.types.core import Tensor from typing import Structure PixellatedRetroImage = Structure[ Tensor, {dtype=np.float32, shape=[640, 480]}] def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ... Even better, right? To avoid conflicts with user-defined "typing.Annotated" type hints that already exist in the wild, we define a new "typing.Structure" class suitable for type-hinting data structure constraints. We then define two constraints in an extensible manner supporting forward-compatible changes, improvements, and modifications to third-party APIs. Of course, that's probably not quite "typing" enough. Right? Not quite enough square braces above and more than a little hostile to static analysis, which is fair enough. So let's delve one step deeper into the cavernous labyrinth of awesomeness that solves all tensor typing concerns: import numpy as np from tensorflow.types.core import Tensor from typing import Structure, Dtype, Shape PixellatedRetroImage = Structure[ Tensor, Dtype[np.float32], Shape[640, 480]] def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ... Yup. It doesn't get more "typing" than that. You've got square braces all over the place; you've got well-named constraints generically applicable to the gamut of popular frameworks; you've got order-invariant extensibility, because it no longer matters what order users declare "Dtype[...]" and "Shape[...]" constraints in (or whether users even declare those constraints); you've got built-in support for both static and runtime analysis. And you've got all that without burdening third-party vendors with a Python-specific C[++]-unfriendly API they're unlikely to ever adopt. In short, you've got everything you could probably, possibly, or likely ever want. ...which means it'll never happen. PEP 646 is all but a fait accompli at this eleventh hour. That's fine, I guess. But that's also kinda *not* fine, you know? We can and should do substantially better. I reckon we can. In closing, thanks for all the Pythonic goodness over the past several decades. Everyone's amazing! Regardless of where and how Python type-checking intersects with scientific computing in the decades to come, I'm indefatigably proud of everything everyone's accomplished here. Keep on rocking the type-safe world.
TL;DR: runtime type-checkers can’t use stubs easily, and types using PEP 646 can’t be extended in a backward-compatible way (since they flatten all of the parameters into one namespace-less list).
On Tue, Apr 13, 2021 at 6:58 AM Cecil Curry <leycec@gmail.com> wrote:
This includes popular numerical frameworks one might erroneously assume from their names to be Python-specific like PyTorch. Despite the name, PyTorch is also implemented as a low-level Python-agnostic C++ layer (https://pytorch.org/tutorials/advanced/cpp_frontend.html). The same goes for:
* The entirety of TensorFlow's core C++ API ( https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/framewo... ). * *Most* but not all of NumPy's core C API (https://numpy.org/doc/stable/reference/c-api/coremath.html), which NumPy developers are desperately refactoring to be Python-agnostic. I believe the core "numpy.core._multiarray_umath.ndarray" type is still Python-specific, but that's likely to change sometime this decade. (Don't quote me on that. Please. My GitHub karma can only take so many body blows.)
As a member of the NumPy core team, let me set the record straight: NumPy is not and will never be Python agnostic. NumPy's internals are specialized to Python's C API. There are many wonderful multi-language numerical computing frameworks, but NumPy is not one of them. I'm not sure where you got this idea but it isn't true. From a performance perspective, I doubt there are any fundamental issues. Python itself (since Python 3.9) comes with quite a few types defined in C that support type annotations. The ndarray objects in NumPy and other numerical computing frameworks are quite similar in spirit and implementation to Python's built-in types. Your extensibility concerns are legitimate, but they aren't really specific to multi-dimensional arrays. The data structures behind NumPy have been around for decades, again just like Python's builtins.
Thanks for the comments, Cecil! To summarize, it sounds like the issues are: 1. It's unclear how the non-Python backends of various libraries would utilise (or perhaps even implement) support for PEP 646. 2. Runtime type checkers can't use stubs in .pyi files. 3. PEP 646 is relatively inflexible in the kinds of constraints on tensors it can encode. 4. That inflexibility might cause problems down the line: if we want to add new kinds of constraints, it's going to require such a major upheaval as to break backwards-compatibility. 5. In general, it would be better to aim for an approach that doesn't require TensorFlow, NumPy etc to do things differently to get good things happening. As a particular alternative, we're talking about using either `Annotated` from PEP 593, or thinking about instead creating a tensor-like-thing-specific `Structure` thing. --- Before responding to these individually, I want to speak to a higher-level crux. I think there are actually two questions here: 1. Is PEP 646 a good thing for Python in general? 2. Should we endorse PEP 646 as the answer to tensor typing in particular, and therefore try to encourage libraries like TensorFlow and NumPy to adopt it? I believe the answer to the first question is "Yes". Variadic generics have plenty of other use cases (https://github.com/python/typing/issues/193), and although we wrote PEP 646 with tensor typing in mind, PEP 646 should still be usable for all these other cases too. PEP 646 will (hopefully) benefit folks other than 'us' For the second question, though - I agree it's less clear whether the answer should be "Yes". To be clear, I'm not going to be advocating to the authors of TensorFlow etc that they should adopt PEP 646 any time soon (as grateful as I am for Stephan's endorsement of the PEP). I think it's too soon to say whether the way we suggest in the examples of PEP 646 *is* the way that we should lock ourselves into. My personal motivation in pushing this forward is: hey, it's plausible it *could* be the right way (or at least a good enough way), so let's get things to a stage where the friction is low enough that enough people try it that we can get feedback on what works and what doesn't for tensor typing. And we *can* get feedback even if TensorFlow etc don't adopt it, through stub libraries like TensorAnnotations <https://github.com/deepmind/tensor_annotations>. (Is it the *best* way to get feedback? Couldn't we get useful feedback in a much easier way with approaches like Beartype and torchtyping <https://github.com/patrick-kidger/torchtyping/tree/master/torchtyping>? I think it depends how much you care about static analysis. Personally I do care quite a lot about it. But that's not to say I'm not excited about runtime methods too - and in fact I'll be adding a runtime checker to TensorAnnotations soon as well.) (A reasonable counterargument is: we don't *really* need PEP 646 to test this hypothesis with e.g. TensorAnnotations. We can make do with type aliases for different ranks, like `Tensor2`, `Tensor3` and so on. I'm sympathetic to this counterargument. My original thinking was that the need for aliases is likely to create too much friction/general feelings of ickiness among users, such that we wouldn't be able to "run the experiment" properly. But I could be over-estimating the effect here.) ---
1. It's unclear how the non-Python backends of various libraries would utilise (or perhaps even implement) support for PEP 646.
This I'm actually confused about. Are classes like `Tensor` and `ndarray` really defined in C or C++? I assumed the classes themselves were defined in Python. (Though admittedly I don't know whether it would be as simple as just changing the class definition - e.g. https://github.com/tensorflow/tensorflow/issues/12345 suggests there might be some blockers to making `Tensor` generic in TensorFlow.)
2. Runtime type checkers can't use stubs in .pyi files.
Interesting - I hadn't thought about this. I'm expecting that most of what runtime checkers care about will be the types of the tensor objects we're passing around - say, if a function is annotated as taking a float32 tensor, is the tensor's dtype really float32? The information in stubs - e.g. that some library function is supposed to permute the axes in a certain way - will be less important. But am I missing something here - what do you think?
3. PEP 646 is relatively inflexible in the kinds of constraints on tensors it can encode. 4. That inflexibility might cause problems down the line: if we want to add new kinds of constraints, it's going to require such a major upheaval as to break backwards-compatibility.
These are fair points, and part of why I think it'd be good to gather more feedback before advocating for uptake by e.g. TensorFlow and NumPy. Having said that, I also don't want us to be *too* perfectionist about it. We could easily spend a *long* time trying to come up with something that was perfectly future-compatible.
As a particular alternative, we're talking about using either `Annotated` from PEP 593, or thinking about instead creating a tensor-like-thing-specific `Structure` thing.
Right; we (some colleagues and I who were talking about it last year) did consider `Annotated` at first - but iirc I changed my mind because, man, we're *so* close to being able to do cool stuff with standard type checkers - whereas `Annotated` or the like would require new tooling if we wanted static analysis. Having said that, I would be very excited to see the `Annotated` approach explored more thoroughly - e.g. if someone proposed a standard set of annotations to get us going, and implemented a runtime checker for them. I'd also be excited to see someone write a more detailed proposal for something like `Structure`. --- So I think in summary, the two key points are: - We should absolutely take our time to see how PEP 646-like tensor typing works in the real world before locking ourselves into anything, and in particular before advocating adoption by big libraries. - Whether you like the PEP 646 way of doing things probably depends a lot on whether you like static analysis. If you don't care so much about static analysis, then I totally agree, it's inflexible compared to what you can do if you're doing your verification with a runtime checker. If you *do *care about static analysis, then it's a yay, because you get to use existing tooling. On Tue, 13 Apr 2021 at 14:58, Cecil Curry <leycec@gmail.com> wrote:
PEP 646: It Probably Doesn't Do What Everyone Wants It To Do
Hi, all! Warmest greetings on this drizzly Spring evening from the squalid Canadian wetlands, where even the mosquitoes think it's really too cold.
I'm Cecil Curry (@leycec), the plucky author of beartype (https://github.com/beartype/beartype) – the pure-Python O(1) runtime type checker that pretends to do alot while actually doing not too much at all. Thus O(1). "You gets what you pays for," right?
My wife and I also co-authored a several million-line pure-Python multiphysics biology simulator (https://gitlab.com/betse/betse) heavily leveraging the standard scientific stack (e.g., NumPy, SciPy, Matplotlib, NetworkX). Those were good years. I still had hair back then, so they had to have been good.
I was kindly redirected here by Ryan Soklaski (@rsokl), the brilliant @beartype user I never knew I had and head instructor of the CogWorks course at the MIT Beaver Works Summer Institute. Ryan accurately predicted that PEP 646 specifically and the vigorous discussion here surrounding tensor typing in general would be right up my lakeside wheelhouse. Boy, is it! We're all over this sort of deep structural science-oriented QA.
In fact, @beartype's userbase ( https://github.com/beartype/beartype/stargazers) is predominated by machine learning, computer vision, and data scientists. We're poised on the awkward precipice between typing and tensors, because what everyone (including my wife and I) always wanted from @beartype was runtime type checking of scientific data structures.
PEP 646 promises all this... and more! But does it deliver? Does @leycec ever regain his hair? Do Canadian brown bears groom themselves on spindly trees on our front yard? The answers to all these questions and more follow shortly.
First, I'd like to profusely thank all the PEP 646 authors – Mark Mendoza, Matthew Rahtz, Pradeep Kumar Srinivasan, Vincent Siles, and Guido van Rossum – for their profuse work and dedication to the good cause.
Second, I'd like to profusely apologize a priori for any untoward argumentation or firework shows I am about to generate. That's not the intention. Really! PEP 646 is currently at the steering committee stage. Nobody wants to see that derailed, because hard work deserves its just reward and GitHub karma.
All that said... let's do this.
The real-world motivation of PEP 646 is to convince the various authors of various popular numerical frameworks to adopt, embrace, and extend PEP 646. This is a noble effort. But can this effort actually be realized?
The PEP 646 API assumes a Python-centric worldview. Specifically, PEP 646 assumes that numerical frameworks will subclass their data structures from "typing.Generic", a magical pure-Python class. Those assumptions derive from the first example in PEP 646:
from typing import TypeVar, TypeVarTuple
DType = TypeVar('DType') Shape = TypeVarTuple('Shape')
class Array(Generic[DType, *Shape]):
def __abs__(self) -> Array[DType, *Shape]: ...
def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: ...
But this assumption may not necessarily hold. Most popular numerical frameworks worth supporting (including numerical frameworks explicitly referenced in PEP 646, which means NumPy and TensorFlow) are implemented with at least two layers: a low-level mostly Python-agnostic layer implemented in some combination of C, C++, Fortran, or Rust referred to as the "core API" and one or more high-level layers providing bindings to high-level languages. Python is only one of many high-level languages supported by these frameworks.
This includes popular numerical frameworks one might erroneously assume from their names to be Python-specific like PyTorch. Despite the name, PyTorch is also implemented as a low-level Python-agnostic C++ layer (https://pytorch.org/tutorials/advanced/cpp_frontend.html). The same goes for:
* The entirety of TensorFlow's core C++ API ( https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/framewo... ). * *Most* but not all of NumPy's core C API (https://numpy.org/doc/stable/reference/c-api/coremath.html), which NumPy developers are desperately refactoring to be Python-agnostic. I believe the core "numpy.core._multiarray_umath.ndarray" type is still Python-specific, but that's likely to change sometime this decade. (Don't quote me on that. Please. My GitHub karma can only take so many body blows.)
Is this a problem? It might very well be. Of course, it should be admitted at this crucial juncture that my idea of a fun time includes scrambling over tick-infested shrubland in an inhospitable wasteland for eight hours yesterday, which only goes to show that you can't trust anyone who voluntarily lives in a poorly insulated cottage plagued with mosquitoes. </ahem>
"typing.Generic" is magical, because it defines the magical PEP 487 __init_subclass__() dunder method. Despite banging my balding head against CPython documentation and my ketchup-stained keyboard for several hours, I'm kinda unconvinced that a C[++]-based class can even subclass one or more magical or non-magical pure-Python classes. I've never seen anyone do either. Then again, I've never seen a circumpolar double rainbow on Christmas Eve composed of flying winged bugbears. Who's to say that can't happen? I'd sure like to see that someday.
So let's assume for the sake of debate that's feasible – that C[++]-based classes can in fact subclass one or more magical pure-Python classes. Under that generous assumption, it's still unlikely that numerical frameworks that were either Python-agnostic from the get-go (like TensorFlow) or are rapidly refactoring their core APIs away from Python (like NumPy) would revert those fundamental decisions just to appease our small but vocal cadre of Python type-checking aficionados. (That's us: the good guys in the distinct minority.)
Now I *know* what you're thinking. "Ah-ha! You poor sad Canadian hillbilly. All we need to do is strenuously encourage the authors of these numerical frameworks to define entirely new Python-specific classes in their Python bindings that comply with PEP 646 by subclassing "typing.Generic" in the exact way we want them to! It's so blindingly simple! If only you still had luxurious hair, @leycec. Then you might have thought of that. Alas. Here we are."
In the dark ruminations of your monitor-lit devcave, you may even be dangerously considering unspeakable horrors like adding a new type-checking shim like this into the perfidious bowels of the NumPy codebase:
from numpy.core._multiarray_umath import ndarray from typing import TypeVar, TypeVarTuple
DType = TypeVar('DType') Shape = TypeVarTuple('Shape')
class NDArray(ndarray, Generic[DType, *Shape]):
def __abs__(self) -> Array[DType, *Shape]: return super().__abs__()
def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: return super().__add__(other)
Right? Something unmentionable like that, right? Of course, we can all admit to ourselves that's horrible, because that imposes one additional stack frame per method call. So, nobody will do that.
Now I *know* what you're thinking. "Ah-ha! You poor sad Canadian person of indeterminate mental stability. All we need to do is define .pyi-formatted stub files instead expressing the same type semantics! If only your limbs weren't painfully arthritic, @leycec. Then you might have thought of that. Alas. Here we are."
Right? Something inexpressible like that, right? Of course, we can all admit to ourselves that's horrible too, because .pyi-formatted stub files aren't sanely accessible at runtime. And we are back where we started in a cursed reenactment of "Blair Witch Project: Tensor Typing Edition."
You're now thinking of either bolting type hints onto C[++]-based callables or wholly reimplementing "typing.Generic" in C, aren't you? I knew it! You are! And at this point, everyone should be thinking: "This doesn't quite seem right anymore."
The core issue here is that PEP 646 requires someone who is not us to change their workflow for us. It's not necessarily clear that that will ever happen. Sure, it might. I'm an optimistic man, which means I'm not a betting man.
Even if that utopic Xanadu were to become manifest, PEP 646 still seems problematic. My PEP 484 isn't strong, but it's unclear how you would:
* Type tensor methods returning new tensors satisfying different constraints (especially of differing shapes). * Type tensor constraints in a forward-compatible manner. Tensor constraints declared by PEP 646 are crazy fragile. They lock any numerical framework that complies into a specific preordained ordering of tensor constraints. A tensor typed as "class Array(Generic[DType, *Shape])" can *never* be typed as anything else for all time, which is a pretty long time. Want to add another constraint like, say, PyTorch's newly introduced "layout" tensor attribute that fundamentally changes how one uses passed or returned tensors? ( https://pytorch.org/docs/stable/tensor_attributes.html#torch.torch.layout) **Sorry.** No one can ever do that, because the original specification of "class Array(Generic[DType, *Shape])" prematurely locked everyone into checking only those two constraints.
Surely we have sane alternatives? We do. Many! Oodles! Bucket-loads! And other colourful idioms for "lots." They all share one attribute in common: they don't require someone who is not us to change their fundamental workflow for us.
The PEP 593 "typing.Annotated" class offers an illustrative example:
import numpy as np from tensorflow.types.core import Tensor from typing import Annotated
PixellatedRetroImage = Annotated[Tensor, np.float32, [640, 480]]
def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ...
That has the benefit of working now under Python >= 3.9 without necessitating that external authors, open-source communities, or for-profit corporations do unlikely (and perhaps infeasible) stuff for us. We'll be pursuing a similar solution to that in @beartype, because it works now and satisfies everyone's downstream concerns.
Of course, "typing" wouldn't want to do something like that. "typing" would want to do something even more robust, extensible, and absurdly awesome like:
import numpy as np from tensorflow.types.core import Tensor from typing import Structure
PixellatedRetroImage = Structure[ Tensor, {dtype=np.float32, shape=[640, 480]}]
def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ...
Even better, right? To avoid conflicts with user-defined "typing.Annotated" type hints that already exist in the wild, we define a new "typing.Structure" class suitable for type-hinting data structure constraints. We then define two constraints in an extensible manner supporting forward-compatible changes, improvements, and modifications to third-party APIs.
Of course, that's probably not quite "typing" enough. Right? Not quite enough square braces above and more than a little hostile to static analysis, which is fair enough. So let's delve one step deeper into the cavernous labyrinth of awesomeness that solves all tensor typing concerns:
import numpy as np from tensorflow.types.core import Tensor from typing import Structure, Dtype, Shape
PixellatedRetroImage = Structure[ Tensor, Dtype[np.float32], Shape[640, 480]]
def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ...
Yup. It doesn't get more "typing" than that. You've got square braces all over the place; you've got well-named constraints generically applicable to the gamut of popular frameworks; you've got order-invariant extensibility, because it no longer matters what order users declare "Dtype[...]" and "Shape[...]" constraints in (or whether users even declare those constraints); you've got built-in support for both static and runtime analysis. And you've got all that without burdening third-party vendors with a Python-specific C[++]-unfriendly API they're unlikely to ever adopt. In short, you've got everything you could probably, possibly, or likely ever want.
...which means it'll never happen. PEP 646 is all but a fait accompli at this eleventh hour. That's fine, I guess. But that's also kinda *not* fine, you know? We can and should do substantially better.
I reckon we can.
In closing, thanks for all the Pythonic goodness over the past several decades. Everyone's amazing! Regardless of where and how Python type-checking intersects with scientific computing in the decades to come, I'm indefatigably proud of everything everyone's accomplished here. Keep on rocking the type-safe world. _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: mrahtz@google.com
Beyond the things Matthew pointed out:
Runtime type checkers can't use stubs in .pyi files.
types using PEP 646 can’t be extended in a backward-compatible way (since
This is not specific to PEP 646. For example, `sqlalchemy` classes are not generic, but their stubs are [1]. There are many other classes that are unannotated but are given specific types in .pyi files. In general, to get the same types that static typecheckers do, runtime typecheckers would have to find a way to access the types from the stubs. [1]: example: the Column class - https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/sql/sche..., stub - https://github.com/dropbox/sqlalchemy-stubs/blob/master/sqlalchemy-stubs/sql... they flatten all of the parameters into one namespace-less list). This again seems like a more general concern about Python types, not specific to PEP 646. Perhaps in the future we could have named generic parameters. That would allow `Tensor[dtype=float32, shape=Tuple[L[32], L[48]]]`, which could be extended to `Tensor[dtype=float32, shape=Tuple[L[32], L[48]], new_thing=int]`. But I don't see this being a blocker here. The datatype and shape look like the most important parts of numerical computing data structures that we want to check statically. Related: we shelved the idea of having a class be generic in multiple `TypeVarTuple`s since we wanted to keep the base PEP simple. We can revisit that in follow-up PEPs based on real-world experiences.
[How to type] Type tensor methods returning new tensors satisfying different constraints (especially of differing shapes).
The PEP has examples of functions returning tensors of a different shape from the argument: https://www.python.org/dev/peps/pep-0646/#type-concatenation On Tue, Apr 13, 2021 at 11:38 AM Matthew Rahtz via Typing-sig < typing-sig@python.org> wrote:
Thanks for the comments, Cecil! To summarize, it sounds like the issues are:
1. It's unclear how the non-Python backends of various libraries would utilise (or perhaps even implement) support for PEP 646. 2. Runtime type checkers can't use stubs in .pyi files. 3. PEP 646 is relatively inflexible in the kinds of constraints on tensors it can encode. 4. That inflexibility might cause problems down the line: if we want to add new kinds of constraints, it's going to require such a major upheaval as to break backwards-compatibility. 5. In general, it would be better to aim for an approach that doesn't require TensorFlow, NumPy etc to do things differently to get good things happening.
As a particular alternative, we're talking about using either `Annotated` from PEP 593, or thinking about instead creating a tensor-like-thing-specific `Structure` thing.
---
Before responding to these individually, I want to speak to a higher-level crux. I think there are actually two questions here:
1. Is PEP 646 a good thing for Python in general? 2. Should we endorse PEP 646 as the answer to tensor typing in particular, and therefore try to encourage libraries like TensorFlow and NumPy to adopt it?
I believe the answer to the first question is "Yes". Variadic generics have plenty of other use cases ( https://github.com/python/typing/issues/193), and although we wrote PEP 646 with tensor typing in mind, PEP 646 should still be usable for all these other cases too. PEP 646 will (hopefully) benefit folks other than 'us'
For the second question, though - I agree it's less clear whether the answer should be "Yes". To be clear, I'm not going to be advocating to the authors of TensorFlow etc that they should adopt PEP 646 any time soon (as grateful as I am for Stephan's endorsement of the PEP). I think it's too soon to say whether the way we suggest in the examples of PEP 646 *is* the way that we should lock ourselves into. My personal motivation in pushing this forward is: hey, it's plausible it *could* be the right way (or at least a good enough way), so let's get things to a stage where the friction is low enough that enough people try it that we can get feedback on what works and what doesn't for tensor typing. And we *can* get feedback even if TensorFlow etc don't adopt it, through stub libraries like TensorAnnotations <https://github.com/deepmind/tensor_annotations>.
(Is it the *best* way to get feedback? Couldn't we get useful feedback in a much easier way with approaches like Beartype and torchtyping <https://github.com/patrick-kidger/torchtyping/tree/master/torchtyping>? I think it depends how much you care about static analysis. Personally I do care quite a lot about it. But that's not to say I'm not excited about runtime methods too - and in fact I'll be adding a runtime checker to TensorAnnotations soon as well.)
(A reasonable counterargument is: we don't *really* need PEP 646 to test this hypothesis with e.g. TensorAnnotations. We can make do with type aliases for different ranks, like `Tensor2`, `Tensor3` and so on. I'm sympathetic to this counterargument. My original thinking was that the need for aliases is likely to create too much friction/general feelings of ickiness among users, such that we wouldn't be able to "run the experiment" properly. But I could be over-estimating the effect here.)
---
1. It's unclear how the non-Python backends of various libraries would utilise (or perhaps even implement) support for PEP 646.
This I'm actually confused about. Are classes like `Tensor` and `ndarray` really defined in C or C++? I assumed the classes themselves were defined in Python. (Though admittedly I don't know whether it would be as simple as just changing the class definition - e.g. https://github.com/tensorflow/tensorflow/issues/12345 suggests there might be some blockers to making `Tensor` generic in TensorFlow.)
2. Runtime type checkers can't use stubs in .pyi files.
Interesting - I hadn't thought about this.
I'm expecting that most of what runtime checkers care about will be the types of the tensor objects we're passing around - say, if a function is annotated as taking a float32 tensor, is the tensor's dtype really float32? The information in stubs - e.g. that some library function is supposed to permute the axes in a certain way - will be less important. But am I missing something here - what do you think?
3. PEP 646 is relatively inflexible in the kinds of constraints on tensors it can encode. 4. That inflexibility might cause problems down the line: if we want to add new kinds of constraints, it's going to require such a major upheaval as to break backwards-compatibility.
These are fair points, and part of why I think it'd be good to gather more feedback before advocating for uptake by e.g. TensorFlow and NumPy.
Having said that, I also don't want us to be *too* perfectionist about it. We could easily spend a *long* time trying to come up with something that was perfectly future-compatible.
As a particular alternative, we're talking about using either `Annotated` from PEP 593, or thinking about instead creating a tensor-like-thing-specific `Structure` thing.
Right; we (some colleagues and I who were talking about it last year) did consider `Annotated` at first - but iirc I changed my mind because, man, we're *so* close to being able to do cool stuff with standard type checkers - whereas `Annotated` or the like would require new tooling if we wanted static analysis.
Having said that, I would be very excited to see the `Annotated` approach explored more thoroughly - e.g. if someone proposed a standard set of annotations to get us going, and implemented a runtime checker for them. I'd also be excited to see someone write a more detailed proposal for something like `Structure`.
---
So I think in summary, the two key points are:
- We should absolutely take our time to see how PEP 646-like tensor typing works in the real world before locking ourselves into anything, and in particular before advocating adoption by big libraries. - Whether you like the PEP 646 way of doing things probably depends a lot on whether you like static analysis. If you don't care so much about static analysis, then I totally agree, it's inflexible compared to what you can do if you're doing your verification with a runtime checker. If you *do *care about static analysis, then it's a yay, because you get to use existing tooling.
On Tue, 13 Apr 2021 at 14:58, Cecil Curry <leycec@gmail.com> wrote:
PEP 646: It Probably Doesn't Do What Everyone Wants It To Do
Hi, all! Warmest greetings on this drizzly Spring evening from the squalid Canadian wetlands, where even the mosquitoes think it's really too cold.
I'm Cecil Curry (@leycec), the plucky author of beartype (https://github.com/beartype/beartype) – the pure-Python O(1) runtime type checker that pretends to do alot while actually doing not too much at all. Thus O(1). "You gets what you pays for," right?
My wife and I also co-authored a several million-line pure-Python multiphysics biology simulator (https://gitlab.com/betse/betse) heavily leveraging the standard scientific stack (e.g., NumPy, SciPy, Matplotlib, NetworkX). Those were good years. I still had hair back then, so they had to have been good.
I was kindly redirected here by Ryan Soklaski (@rsokl), the brilliant @beartype user I never knew I had and head instructor of the CogWorks course at the MIT Beaver Works Summer Institute. Ryan accurately predicted that PEP 646 specifically and the vigorous discussion here surrounding tensor typing in general would be right up my lakeside wheelhouse. Boy, is it! We're all over this sort of deep structural science-oriented QA.
In fact, @beartype's userbase ( https://github.com/beartype/beartype/stargazers) is predominated by machine learning, computer vision, and data scientists. We're poised on the awkward precipice between typing and tensors, because what everyone (including my wife and I) always wanted from @beartype was runtime type checking of scientific data structures.
PEP 646 promises all this... and more! But does it deliver? Does @leycec ever regain his hair? Do Canadian brown bears groom themselves on spindly trees on our front yard? The answers to all these questions and more follow shortly.
First, I'd like to profusely thank all the PEP 646 authors – Mark Mendoza, Matthew Rahtz, Pradeep Kumar Srinivasan, Vincent Siles, and Guido van Rossum – for their profuse work and dedication to the good cause.
Second, I'd like to profusely apologize a priori for any untoward argumentation or firework shows I am about to generate. That's not the intention. Really! PEP 646 is currently at the steering committee stage. Nobody wants to see that derailed, because hard work deserves its just reward and GitHub karma.
All that said... let's do this.
The real-world motivation of PEP 646 is to convince the various authors of various popular numerical frameworks to adopt, embrace, and extend PEP 646. This is a noble effort. But can this effort actually be realized?
The PEP 646 API assumes a Python-centric worldview. Specifically, PEP 646 assumes that numerical frameworks will subclass their data structures from "typing.Generic", a magical pure-Python class. Those assumptions derive from the first example in PEP 646:
from typing import TypeVar, TypeVarTuple
DType = TypeVar('DType') Shape = TypeVarTuple('Shape')
class Array(Generic[DType, *Shape]):
def __abs__(self) -> Array[DType, *Shape]: ...
def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: ...
But this assumption may not necessarily hold. Most popular numerical frameworks worth supporting (including numerical frameworks explicitly referenced in PEP 646, which means NumPy and TensorFlow) are implemented with at least two layers: a low-level mostly Python-agnostic layer implemented in some combination of C, C++, Fortran, or Rust referred to as the "core API" and one or more high-level layers providing bindings to high-level languages. Python is only one of many high-level languages supported by these frameworks.
This includes popular numerical frameworks one might erroneously assume from their names to be Python-specific like PyTorch. Despite the name, PyTorch is also implemented as a low-level Python-agnostic C++ layer (https://pytorch.org/tutorials/advanced/cpp_frontend.html). The same goes for:
* The entirety of TensorFlow's core C++ API ( https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/framewo... ). * *Most* but not all of NumPy's core C API (https://numpy.org/doc/stable/reference/c-api/coremath.html), which NumPy developers are desperately refactoring to be Python-agnostic. I believe the core "numpy.core._multiarray_umath.ndarray" type is still Python-specific, but that's likely to change sometime this decade. (Don't quote me on that. Please. My GitHub karma can only take so many body blows.)
Is this a problem? It might very well be. Of course, it should be admitted at this crucial juncture that my idea of a fun time includes scrambling over tick-infested shrubland in an inhospitable wasteland for eight hours yesterday, which only goes to show that you can't trust anyone who voluntarily lives in a poorly insulated cottage plagued with mosquitoes. </ahem>
"typing.Generic" is magical, because it defines the magical PEP 487 __init_subclass__() dunder method. Despite banging my balding head against CPython documentation and my ketchup-stained keyboard for several hours, I'm kinda unconvinced that a C[++]-based class can even subclass one or more magical or non-magical pure-Python classes. I've never seen anyone do either. Then again, I've never seen a circumpolar double rainbow on Christmas Eve composed of flying winged bugbears. Who's to say that can't happen? I'd sure like to see that someday.
So let's assume for the sake of debate that's feasible – that C[++]-based classes can in fact subclass one or more magical pure-Python classes. Under that generous assumption, it's still unlikely that numerical frameworks that were either Python-agnostic from the get-go (like TensorFlow) or are rapidly refactoring their core APIs away from Python (like NumPy) would revert those fundamental decisions just to appease our small but vocal cadre of Python type-checking aficionados. (That's us: the good guys in the distinct minority.)
Now I *know* what you're thinking. "Ah-ha! You poor sad Canadian hillbilly. All we need to do is strenuously encourage the authors of these numerical frameworks to define entirely new Python-specific classes in their Python bindings that comply with PEP 646 by subclassing "typing.Generic" in the exact way we want them to! It's so blindingly simple! If only you still had luxurious hair, @leycec. Then you might have thought of that. Alas. Here we are."
In the dark ruminations of your monitor-lit devcave, you may even be dangerously considering unspeakable horrors like adding a new type-checking shim like this into the perfidious bowels of the NumPy codebase:
from numpy.core._multiarray_umath import ndarray from typing import TypeVar, TypeVarTuple
DType = TypeVar('DType') Shape = TypeVarTuple('Shape')
class NDArray(ndarray, Generic[DType, *Shape]):
def __abs__(self) -> Array[DType, *Shape]: return super().__abs__()
def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: return super().__add__(other)
Right? Something unmentionable like that, right? Of course, we can all admit to ourselves that's horrible, because that imposes one additional stack frame per method call. So, nobody will do that.
Now I *know* what you're thinking. "Ah-ha! You poor sad Canadian person of indeterminate mental stability. All we need to do is define .pyi-formatted stub files instead expressing the same type semantics! If only your limbs weren't painfully arthritic, @leycec. Then you might have thought of that. Alas. Here we are."
Right? Something inexpressible like that, right? Of course, we can all admit to ourselves that's horrible too, because .pyi-formatted stub files aren't sanely accessible at runtime. And we are back where we started in a cursed reenactment of "Blair Witch Project: Tensor Typing Edition."
You're now thinking of either bolting type hints onto C[++]-based callables or wholly reimplementing "typing.Generic" in C, aren't you? I knew it! You are! And at this point, everyone should be thinking: "This doesn't quite seem right anymore."
The core issue here is that PEP 646 requires someone who is not us to change their workflow for us. It's not necessarily clear that that will ever happen. Sure, it might. I'm an optimistic man, which means I'm not a betting man.
Even if that utopic Xanadu were to become manifest, PEP 646 still seems problematic. My PEP 484 isn't strong, but it's unclear how you would:
* Type tensor methods returning new tensors satisfying different constraints (especially of differing shapes). * Type tensor constraints in a forward-compatible manner. Tensor constraints declared by PEP 646 are crazy fragile. They lock any numerical framework that complies into a specific preordained ordering of tensor constraints. A tensor typed as "class Array(Generic[DType, *Shape])" can *never* be typed as anything else for all time, which is a pretty long time. Want to add another constraint like, say, PyTorch's newly introduced "layout" tensor attribute that fundamentally changes how one uses passed or returned tensors? ( https://pytorch.org/docs/stable/tensor_attributes.html#torch.torch.layout ) **Sorry.** No one can ever do that, because the original specification of "class Array(Generic[DType, *Shape])" prematurely locked everyone into checking only those two constraints.
Surely we have sane alternatives? We do. Many! Oodles! Bucket-loads! And other colourful idioms for "lots." They all share one attribute in common: they don't require someone who is not us to change their fundamental workflow for us.
The PEP 593 "typing.Annotated" class offers an illustrative example:
import numpy as np from tensorflow.types.core import Tensor from typing import Annotated
PixellatedRetroImage = Annotated[Tensor, np.float32, [640, 480]]
def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ...
That has the benefit of working now under Python >= 3.9 without necessitating that external authors, open-source communities, or for-profit corporations do unlikely (and perhaps infeasible) stuff for us. We'll be pursuing a similar solution to that in @beartype, because it works now and satisfies everyone's downstream concerns.
Of course, "typing" wouldn't want to do something like that. "typing" would want to do something even more robust, extensible, and absurdly awesome like:
import numpy as np from tensorflow.types.core import Tensor from typing import Structure
PixellatedRetroImage = Structure[ Tensor, {dtype=np.float32, shape=[640, 480]}]
def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ...
Even better, right? To avoid conflicts with user-defined "typing.Annotated" type hints that already exist in the wild, we define a new "typing.Structure" class suitable for type-hinting data structure constraints. We then define two constraints in an extensible manner supporting forward-compatible changes, improvements, and modifications to third-party APIs.
Of course, that's probably not quite "typing" enough. Right? Not quite enough square braces above and more than a little hostile to static analysis, which is fair enough. So let's delve one step deeper into the cavernous labyrinth of awesomeness that solves all tensor typing concerns:
import numpy as np from tensorflow.types.core import Tensor from typing import Structure, Dtype, Shape
PixellatedRetroImage = Structure[ Tensor, Dtype[np.float32], Shape[640, 480]]
def upscale_image(image: PixellatedRetroImage) -> PixellatedRetroImage: ...
Yup. It doesn't get more "typing" than that. You've got square braces all over the place; you've got well-named constraints generically applicable to the gamut of popular frameworks; you've got order-invariant extensibility, because it no longer matters what order users declare "Dtype[...]" and "Shape[...]" constraints in (or whether users even declare those constraints); you've got built-in support for both static and runtime analysis. And you've got all that without burdening third-party vendors with a Python-specific C[++]-unfriendly API they're unlikely to ever adopt. In short, you've got everything you could probably, possibly, or likely ever want.
...which means it'll never happen. PEP 646 is all but a fait accompli at this eleventh hour. That's fine, I guess. But that's also kinda *not* fine, you know? We can and should do substantially better.
I reckon we can.
In closing, thanks for all the Pythonic goodness over the past several decades. Everyone's amazing! Regardless of where and how Python type-checking intersects with scientific computing in the decades to come, I'm indefatigably proud of everything everyone's accomplished here. Keep on rocking the type-safe world. _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: mrahtz@google.com
_______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: gohanpra@gmail.com
-- S Pradeep Kumar
participants (5)
-
Brandt Bucher
-
Cecil Curry
-
Matthew Rahtz
-
S Pradeep Kumar
-
Stephan Hoyer