Suggestion: a little language for type definitions
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored. Possibly because it’s a really stupid idea, but let me post it again on the off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language for type specifications?
If you look at other programming languages you’ll see that the “type definition sub-language” is often completely different from the “execution sub-language”, with only some symbols in common and used in vaguely related ways. `bool (*myfuncptr)(int, float*)` uses a completely different set of syntactic rules than `rv = (*myfunptr)(*myintptr, &myfloat)`. So with some grains of salt you could say that C is comprised of a declarative typing sublanguage and an imperative execution sublanguage.
And an even better example is Pascal, which uses a set of syntactic constructs for typing that are completely different from the execution statement syntax: `var a : array[1..10] of real` looks very different from `a[1]`, where C `float a[10]` looks pretty similar to `a[10]`. The next bit of my original email is another wild idea, the previous bit doesn’t depend on it really. I can imagine completely different ways of doing a typing sublanguage:
Python typing uses basically a subset of the execution expression syntax as its declarative typing language.
What if we created a little language that is clearly flagged, for example as t”….” or t’….’? Then we could simply define the typestring language to be readable, so you could indeed say t”(int, str) -> bool”. And we could even allow escapes (similar to f-strings) so that the previous expression could also be specified, if you really wanted to, as t”{typing.Callable[[int, str], bool}”.
-- Jack Jansen, <Jack.Jansen@cwi.nl>, http://www.cwi.nl/~jack If I can't dance I don't want to be part of your revolution -- Emma Goldman
On Fri, Jan 7, 2022 at 4:04 PM <jack.jansen@cwi.nl> wrote:
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored.
Maybe not. I made a similar suggestion early in the thread, and Brett Cannon said that the SC had rejected that approach. But I’m not sure when that was— is it time to revisit the idea? Note that if PEP 563 is ultimately accepted, then Annotations would be strings, and type checkers could use any language they wanted. In fact, right now annotations can Optionally be strings anyway. I don’t think that would be good for the community to have multiple sys to do it, but it might be a way to experiment. -CHB Possibly because it’s a really stupid idea, but let me post it again on the
off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language for type specifications?
If you look at other programming languages you’ll see that the “type definition sub-language” is often completely different from the “execution sub-language”, with only some symbols in common and used in vaguely related ways. `bool (*myfuncptr)(int, float*)` uses a completely different set of syntactic rules than `rv = (*myfunptr)(*myintptr, &myfloat)`. So with some grains of salt you could say that C is comprised of a declarative typing sublanguage and an imperative execution sublanguage.
And an even better example is Pascal, which uses a set of syntactic constructs for typing that are completely different from the execution statement syntax: `var a : array[1..10] of real` looks very different from `a[1]`, where C `float a[10]` looks pretty similar to `a[10]`.
The next bit of my original email is another wild idea, the previous bit doesn’t depend on it really. I can imagine completely different ways of doing a typing sublanguage:
Python typing uses basically a subset of the execution expression syntax as its declarative typing language.
What if we created a little language that is clearly flagged, for example as t”….” or t’….’? Then we could simply define the typestring language to be readable, so you could indeed say t”(int, str) -> bool”. And we could even allow escapes (similar to f-strings) so that the previous expression could also be specified, if you really wanted to, as t”{typing.Callable[[int, str], bool}”.
--
Jack Jansen, <Jack.Jansen@cwi.nl>, http://www.cwi.nl/~jack
If I can't dance I don't want to be part of your revolution -- Emma Goldman
_______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/26JB6YIP... Code of Conduct: http://python.org/psf/codeofconduct/
-- Christopher Barker, PhD (Chris) Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython
On Fri, Jan 07, 2022 at 04:27:37PM -0800, Christopher Barker wrote:
Note that if PEP 563 is ultimately accepted, then Annotations would be strings, and type checkers could use any language they wanted.
Annotations still have to be syntactically valid Python expressions. >>> from __future__ import annotations >>> def func(arg: array[1...10] of float) -> str: pass File "<stdin>", line 1 def func(arg: array[1...10] of float) -> str: pass ^^^ SyntaxError: invalid syntax Of course if you explicitly wrap your annotation in quotation marks, you can use any syntax you like (think: regexes, SQL, etc). But without a *standard* annotation syntax: - static checkers will disagree on what annotations mean; - runtime introspection will be difficult; and - IDEs and syntax colourisers are going to just treat them as strings. We can write little DSLs with any syntax we like using explicitly quoted strings. We're not limited to do this in annotations. But while DSLs tend to be specific to your own library or application, annotations exist in a wide ecosystem of both static and runtime tools that expect to interpret annotations. Writing your own little DSL for annotations cuts you off from the rest of the Python ecosystem. -- Steve
This is actually a topic the 2021 SC discussed at length and at the time we decided that the typing language should follow the execution language. It was buried in the call type syntax thread so it’s was probably easy to miss: https://mail.python.org/archives/list/python-dev@python.org/message/4TY3MVJQ... Of course, this decision can be revisited by the 2022 SC. -Barry
On Jan 7, 2022, at 15:59, Jack.Jansen@cwi.nl wrote:
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored. Possibly because it’s a really stupid idea, but let me post it again on the off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language for type specifications?
If you look at other programming languages you’ll see that the “type definition sub-language” is often completely different from the “execution sub-language”, with only some symbols in common and used in vaguely related ways. `bool (*myfuncptr)(int, float*)` uses a completely different set of syntactic rules than `rv = (*myfunptr)(*myintptr, &myfloat)`. So with some grains of salt you could say that C is comprised of a declarative typing sublanguage and an imperative execution sublanguage.
And an even better example is Pascal, which uses a set of syntactic constructs for typing that are completely different from the execution statement syntax: `var a : array[1..10] of real` looks very different from `a[1]`, where C `float a[10]` looks pretty similar to `a[10]`.
The next bit of my original email is another wild idea, the previous bit doesn’t depend on it really. I can imagine completely different ways of doing a typing sublanguage:
Python typing uses basically a subset of the execution expression syntax as its declarative typing language.
What if we created a little language that is clearly flagged, for example as t”….” or t’….’? Then we could simply define the typestring language to be readable, so you could indeed say t”(int, str) -> bool”. And we could even allow escapes (similar to f-strings) so that the previous expression could also be specified, if you really wanted to, as t”{typing.Callable[[int, str], bool}”.
-- Jack Jansen, <Jack.Jansen@cwi.nl>, http://www.cwi.nl/~jack If I can't dance I don't want to be part of your revolution -- Emma Goldman
_______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/26JB6YIP... Code of Conduct: http://python.org/psf/codeofconduct/
08.01.22 01:59, jack.jansen@cwi.nl пише:
If I can make a wild suggestion: why not create a little language for type specifications?
We need a way to define aliases. For example, write: Data = Mapping[str, Sequence[Tuple[int, T]]] Factory = Callable[[int, Iterable[str]], Optional[list[Data[T]]]] def get_foo_factory(type: str, id: int) -> Factory[Foo]: ... instead of def get_foo_factory(type: str, id: int) -> Callable[[int, Iterable[str]], Optional[list[Mapping[str, Sequence[Tuple[int, Foo]]]]]]: ...
Indeed, there needs to be a way to get back and forth from the little typing language to Python and back. That’s why I suggested the t-string format: in analogy to f-strings you could use {expression} constructs in there that would be evaluated in the normal Python environment (and syntax). For the sake of argument lets assume for now that the little language uses - a : b to be equivalent to Mapping[a, b] - [a] to be Sequence[a] - (a, b) being Tuple[a, b] - (a, b) -> c being Callable, - *a being Iterable[a] - ?a being Optional[a] Then your example would become something like T = TypeVar(’T’) Data = t’str : [(int, T)]’ Factory = t’(int, *str) -> ?[Data(T)]’ And note that I’m making up the syntax as I’m typing. Maybe it’s much better to use keywords (like optional, iterable) in stead of symbols). -- Jack Jansen, <Jack.Jansen@cwi.nl>, http://www.cwi.nl/~jack If I can't dance I don't want to be part of your revolution -- Emma Goldman
On 8 Jan 2022, at 11:32, Serhiy Storchaka <storchaka@gmail.com> wrote:
08.01.22 01:59, jack.jansen@cwi.nl пише:
If I can make a wild suggestion: why not create a little language for type specifications?
We need a way to define aliases. For example, write:
Data = Mapping[str, Sequence[Tuple[int, T]]] Factory = Callable[[int, Iterable[str]], Optional[list[Data[T]]]]
def get_foo_factory(type: str, id: int) -> Factory[Foo]: ...
instead of
def get_foo_factory(type: str, id: int) -> Callable[[int, Iterable[str]], Optional[list[Mapping[str, Sequence[Tuple[int, Foo]]]]]]: ...
_______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/42A5UC24... Code of Conduct: http://python.org/psf/codeofconduct/
I find this a really elegant approach. While the SC's decision to keep the syntax uniform is certainly laudable, it's creating the issue of packaging new complexities into a very limited syntactic & semantic space (e.g. no new magic symbols like "->", which I agree with BTW), leaving only very verbose solutions that the typing crowd is chafing against. I think accepting that typing has a syntactic cost on the python language as a whole is unavoidable at some point (and I'm not saying that's a bad thing). Having a separate & opt-in mini-language for type declarations seems like a really clean way to delineate that cost resp. extension, and I especially like the t''-string syntax. Best, H.
While the SC's decision to keep the syntax uniform is certainly laudable, it's creating the issue of packaging new complexities into a very limited syntactic & semantic space (e.g. no new magic symbols like "->", which I agree with BTW), leaving only very verbose solutions that the typing crowd is chafing against.
Seems that I unintentionally ended up jinxing the callable type syntax PEP - I must have misread some of the discussions up until that point, thinking that "->" had been already ruled out - sorry! 😅
I think accepting that typing has a syntactic cost on the python language as a whole is unavoidable at some point (and I'm not saying that's a bad thing). Having a separate & opt-in mini-language for type declarations seems like a really clean way to delineate that cost resp. extension, and I especially like the t''-string syntax.
In light of the rejection of that PEP, I think this point is worth revisiting (in due time).
On Sat, Jan 08, 2022 at 12:32:35PM +0200, Serhiy Storchaka wrote:
08.01.22 01:59, jack.jansen@cwi.nl пише:
If I can make a wild suggestion: why not create a little language for type specifications?
We need a way to define aliases. For example, write:
Data = Mapping[str, Sequence[Tuple[int, T]]] Factory = Callable[[int, Iterable[str]], Optional[list[Data[T]]]]
Can't we already do that? https://docs.python.org/3/library/typing.html#type-aliases Type aliases are great. But there are times where we don't want to write an alias when we can just write the type in-place, just as there are times where we don't want to write a function when we can just use an in-place expression. -- Steve
On 8 Jan 2022 at 00:59:38, jack.jansen@cwi.nl wrote:
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored. Possibly because it’s a really stupid idea, but let me post it again on the off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language for type specifications?
Indeed. Using the same syntax may have some benefits for language implementors (e.g. less complex grammar to implement), but I don’t really see these benefits for language users. As an example, and I don’t know if this has been discussed before, I think a pretty neat syntax construct for optional argument would be (like, for instance, in Kotlin): def f(x: int? = None): ... Instead of: def f(x: Optional[int] = None): … or def f(x: int | None = None): … One could even argue that the “= None” part would be redundant (def f(x: int?): ...) and could be made optional. But that would open another can of worms. S. -- Stefane Fermigier - http://fermigier.com/ - http://twitter.com/sfermigier - http://linkedin.com/in/sfermigier Founder & CEO, Abilian - Enterprise Social Software - http://www.abilian.com/ Co-Founder & Co-Chairman, National Council for Free & Open Source Software (CNLL) - http://cnll.fr/ Co-Founder & Chairman, Association Professionnelle Européenne du Logiciel Libre (APELL) - https://www.apell.info/ Co-Founder & Spokesperson, European Cloud Industrial Alliance (EUCLIDIA) - https://www.euclidia.eu/ Founder, PyParis & PyData Paris - http://pyparis.org/ & http://pydata.fr/
The advantage to users of keeping the languages the same is that readers of your code don’t have to learn two disparate syntaxes to make sense of what they’re reading. One of Python’s enduring strengths has been its readability. In many ways, type annotations challenge that, but the trade-off (so far) has been worth it (IMHO). The trick is to balance the expressability that typing needs with the intuitive understanding of “regular” Python code. FWIW, this is something I struggled with while on the SC during the Pattern Matching debates. I think the right balance was found, ultimately. -Barry On Sat, Jan 8, 2022, at 03:06, Stéfane Fermigier wrote:
On 8 Jan 2022 at 00:59:38, jack.jansen@cwi.nl wrote:
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored. Possibly because it’s a really stupid idea, but let me post it again on the off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language for type specifications?
Indeed.
Using the same syntax may have some benefits for language implementors (e.g. less complex grammar to implement), but I don’t really see these benefits for language users.
As an example, and I don’t know if this has been discussed before, I think a pretty neat syntax construct for optional argument would be (like, for instance, in Kotlin):
def f(x: int? = None): ...
Instead of:
def f(x: Optional[int] = None): …
or
def f(x: int | None = None): …
One could even argue that the “= None” part would be redundant (def f(x: int?): ...) and could be made optional. But that would open another can of worms.
S.
-- Stefane Fermigier - http://fermigier.com/ - http://twitter.com/sfermigier - http://linkedin.com/in/sfermigier Founder & CEO, Abilian - Enterprise Social Software - http://www.abilian.com/ Co-Founder & Co-Chairman, National Council for Free & Open Source Software (CNLL) - http://cnll.fr/ Co-Founder & Chairman, Association Professionnelle Européenne du Logiciel Libre (APELL) - https://www.apell.info/ Co-Founder & Spokesperson, European Cloud Industrial Alliance (EUCLIDIA) - https://www.euclidia.eu/ Founder, PyParis & PyData Paris - http://pyparis.org/ & http://pydata.fr/ _______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/PDRKIUCG... Code of Conduct: http://python.org/psf/codeofconduct/
... make sense of what they’re reading.
Some of us have that problem with type-embellished code now. I'm not sure a little language would be such a bad idea. 🤔 Fortunately, my relationship to the working world allows me to simply ignore explicit typing. 😉 Way, way BITD I recall leaning on a crutch to generate complex C type declarations. I no longer recall what it was called, but you gave it a restricted English description of what you wanted ("function returning pointer to function returning void pointer" or something similar) and it spit out the necessary line noise. Skip
On 1/8/2022 2:05 PM, Skip Montanaro wrote:
... make sense of what they’re reading.
Some of us have that problem with type-embellished code now.I'm not sure a little language would be such a bad idea. 🤔Fortunately, my relationship to the working world allows me to simply ignore explicit typing. 😉
Way, way BITD I recall leaning on a crutch to generate complex C type declarations. I no longer recall what it was called, but you gave it a restricted English description of what you wanted ("function returning pointer to function returning void pointer" or something similar) and it spit out the necessary line noise.
Skip
Oh yes, I remember that... went Googling and found https://cdecl.org/ which is apparently the "modern" online version of that old tool. I guess typing features might be useful for people that refuse to write/read comments, but I mostly ignore the typing stuff too... and hope I can continue to.
On 8 Jan 2022, at 23:05, Skip Montanaro <skip.montanaro@gmail.com> wrote:
... make sense of what they’re reading. Some of us have that problem with type-embellished code now. I'm not sure a little language would be such a bad idea. 🤔 Fortunately, my relationship to the working world allows me to simply ignore explicit typing. 😉
Yeah, that was my take too, until about a year ago. In the last year I’ve contributed mods to two packages that were rejected because I hadn’t provided typing (and black formatting, but that’s a different subject). I’ve reluctantly done so. And while I *hated* it because of the unreadability I _do_ like the outcome: I changed some of the APIs because they were doing things “my way”, but that was really pretty impossible to explain to the typing system. The modified APIs are arguably cleaner. I’m not yet at the point where I’m going to type all the packages and other stuff I maintain, but I _am_ contemplating doing it, eventually, maybe, if I find the time, procrastination willing, …. -- Jack Jansen, <Jack.Jansen@cwi.nl>, http://www.cwi.nl/~jack If I can't dance I don't want to be part of your revolution -- Emma Goldman
On Sun, Jan 09, 2022 at 01:30:33AM +0100, jack.jansen@cwi.nl wrote:
In the last year I’ve contributed mods to two packages that were rejected because I hadn’t provided typing (and black formatting, but that’s a different subject). I’ve reluctantly done so. And while I *hated* it because of the unreadability I _do_ like the outcome
Typing is hard because it is often more abstract and less concrete than the code you are adding type hints to. It is *metaprogramming*. And it involves unfamiliar jargon (covariant, contravariant, typeguards, etc). But the annotations themselves are plain ol' Python expressions. So if you hate type annotations because they are unreadable, then you hate Python because Python is unreadable. There may be some unfamiliarity if you aren't doing a lot of typing (what's a ParamSpec?), and due to the use of square brackets instead of round, but if you can read expressions: spam(eggs | cheese, aardvark) then you can read type expressions: Spam[Eggs | Cheese, Aardvark] Creating a whole new language to describe type hints will go against that. All the existing complexity of typing will still exist, but on top of that, there will *also* be the problem that the syntax used for type expressions will *not be Python* but a second language. Wrapping that second language in t"..." will not change that. -- Steve
On 1/8/22 5:46 PM, Steven D'Aprano wrote:
[...] if you hate type annotations because they are unreadable, then you hate Python because Python is unreadable.
Not so. A simple list comprehension is (usually) quite readable, while a triply-nested list comprehension all on one line is not. Similarly, adding type information in between a variable name and its value is not (for me, and apparently others too) readable. Most horribly of all, cluttering a function header with type information is most unreadable. I started using Python at 2.5. It was simple, clean, and elegant. If I had stumbled on it at 3.16 with samples, tutorials, and books all infused with typing clutter (which *looks* like boiler-plate even if it isn't) I wouldn't have given it a second glance. -- ~Ethan~
On Sat, Jan 08, 2022 at 06:30:53PM -0800, Ethan Furman wrote:
On 1/8/22 5:46 PM, Steven D'Aprano wrote:
[...] if you hate type annotations because they are unreadable, then you hate Python because Python is unreadable.
Not so.
Are you disputing that annotations use the same syntax as other Python expressions? If not, I don't see how you can deny that "type annotations are unreadable" implies "Python expressions are unreadable", which in turn implies "Python is unreadable".
A simple list comprehension is (usually) quite readable, while a triply-nested list comprehension all on one line is not.
Indeed. We can abuse any syntax. So do we conclude that comprehensions are "unreadable" because we can write obfuscated triply-nested list comprehensions?
Similarly, adding type information in between a variable name and its value is not (for me, and apparently others too) readable.
I think that "unreadable" or "not readable" is a complaint that gets overused, often for the most trivial cases, to the point that it loses all credibility. Especially when it comes from people who are fluent in C (which may not be you, Ethan). http://unixwiz.net/techtips/reading-cdecl.html "Easily learned", huh. I think that this is one of the clearest examples of the curse of knowledge as it applies to programming that one could hope to find. Anyway, let's compare: # C int n = 44; # Pascal var n: integer; n := 44; # Typescript var n: number = 44; # Java int n = 44; # Python n: int = 44 There are millions who find the C, Pascal, TypeScript and Java perfectly readable. I don't find it credible that people are incapable of reading the last one. Aside: such a type hint is redundant, as mypy is perfectly capable of inferring that n = 44 makes n an int. Style guides should recommend against such redundancy, and I would certainly flag that in a code review. A better example of a *useful* type hint would be: L: list[str] = []
Most horribly of all, cluttering a function header with type information is most unreadable.
I hear you. Adding redundant or unnecessary type hints to parameters just for the sake of having type hints is just clutter, especially if they are never tested by actually running a type checker over the file. (Untested code is broken code. If not right now, it soon will be.) Fortunately, we have *gradual typing*, and nobody should be forced to use type hints in their projects if they don't want them. Just as we don't make linters mandatory, we don't make typing mandatory either. I think that, outside of very simple functions, once we make the decision to annotate a function, we should space them out: # Better def func(spam: list[str], eggs: float, cheese: str = 'cheddar', aardvark: str|bytes = "", eels: Optional[Tuple[int, str]] = None ) -> Hovercraft: which makes them much easier to read. Trying to cram them all into one line is abuse of the syntax every bit as bad as cramming a triply-nested list comp into one line: # Worse def func(spam: list[str], eggs: float, cheese: str = 'cheddar', aardvark: str|bytes = "", eels: Optional[Tuple[int, str]] = None) -> Hovercraft: I can read it, I just don't want to. It is too much like hard work compared to the previous layout. Even if you don't run a type-checker, those annotations can make useful documentation. (At least *sometimes*.) If the parameter name doesn't make it clear what types are allowed, then the annotation can make it clear. So if you don't use a static checker, you can think of type annotations as introspectable documentation.
I started using Python at 2.5. It was simple, clean, and elegant.
And I started using Python at 1.5, when the syntax was even simpler and cleaner. And to this day I will never forget the first time I read Python code, after being told repeatedly how easy to read it, and I couldn't make head or tails of it. All those colons and square brackets, it might as well have been APL. (Not that I knew what APL was back then.) I knew what a for-loop was, from Pascal, Hypertalk and HP RPN calculators: # Pascal for i := 0 to 10 do begin block; end; # Hypertalk repeat with i = 0 to 10 block end repeat # HP-48 RPN language 0 10 FOR I block NEXT but I kept seeing loops like this in Python: for i in range(11): or worse: for somename in [stuff, thing, another_thing, widget]: and worse of all: for somename in values[1:-1]: Python for loops looked nothing like any for loop I had seen before, and they freaked me out, and at the time (early 1990s) there was no publicly available internet where I could look anything up or ask for help. And then there were the brackets. Why does Python sometimes use round brackets, sometimes curly brackets, and sometimes square brackets? x[a] versus x(a)? Why were there sometimes colons inside square brackets and curly brackets {a:b} but never inside round brackets? What was the difference between [1, 2, 3] and (1, 2, 3)? The whole thing was intimidating, and I just put Python away for about a year and didn't look at it again until I had bought Mark Lutz' "Python Pocket Reference" which helped me make sense of it all. That and his "Learning Python". And never looked back. (Since then, I've often felt that Python has spoiled me from learning other languages. The point I am making here is not that I was a dimwit who couldn't even read Python, but that "easy to read" and "readable" is more a matter of familiarity than an inherent property of the language itself. With enough familiarity, even APL is easy to read.
If I had stumbled on it at 3.16 with samples, tutorials, and books all infused with typing clutter (which *looks* like boiler-plate even if it isn't) I wouldn't have given it a second glance.
And again, I hear you. I too wish people would tone down their enthusiasm for adding typing to examples that don't need type hints. We should remember that type hints are a feature aimed at large code bases, where static typing really is valuable. For three line example functions, not so much, not even as documentation. -- Steve
On Sun, Jan 9, 2022 at 3:47 PM Steven D'Aprano <steve@pearwood.info> wrote:
The point I am making here is not that I was a dimwit who couldn't even read Python, but that "easy to read" and "readable" is more a matter of familiarity than an inherent property of the language itself. With enough familiarity, even APL is easy to read.
Can attest. Not specifically about APL, but there are times when I've watched an expert casually reading off a series of electrical meters or other indicators and immediately knowing what something means. For myself, I've read hex dumps as easily as software code, because specific constructs are incredibly obvious (a series of 32-bit Hollerith strings, for instance, is quite easy to read if you know what you're looking at). The most important feature of Python's type hints, like most other constructs, is knowing where one begins and ends. If you're eyeballing a massive blob of code and you find an open quote character, it should be straight-forward to figure out the extent of the string literal. If you're browsing a function and come across a 'while' loop, you should be able to find the end of that loop. And if you're reading a function's parameters, you should be able to understand how much of it is the annotation and where the next parameter begins. To that extent, I definitely want to keep annotation syntax and Python syntax the same; if there's a new feature needed for annotations, add it to the base language, even without any useful semantics. (Valid syntax without semantics is what we have with the matmul operator. I can syntactically parse "f@g(y)" because I know how the matmul operator works, even without knowing the data types involved. If "x->y" is syntactically valid anywhere in Python code, it's not a problem that there are no core data types for which it's meaningful.) ChrisA
To that extent, I definitely want to keep annotation syntax and Python syntax the same; if there's a new feature needed for annotations, add it to the
Perhaps it's worth remembering that this thread spun off one about adding syntax to Python because the current syntax isn't capable of easily expressing an important type hinting concept (i.e. Callable). So arguing that Python is completely readable for type hints is a bit off-mark, isn't it? The question at hand is whether changes in syntax that are desirable for type hinting should be applied across the board in all of Python. I think the debate boils down to: Is it more clear for readers to have two different (but related) related syntaxes for two different (but related) purposes, or to have one Sytax that is used in different ways? Chris A addresses this specifically: base language, even without any useful semantics. And here's where reasonable people can disagree :-)
(Valid syntax without semantics is what we have with the matmul operator. I can syntactically parse "f@g(y)" because I know how the matmul operator works, even without knowing the data types involved.
If "x->y" is syntactically valid anywhere in Python code, it's not a
Sure: because it's a binary operator like all the other binary operators. problem that there are no core data types for which it's meaningful.) Here's where I'm not so sure -- this looks a lot like a binary operator, but it behaves quite differently. IIUC it would always create a Callable, regardless of what the types were of the two other types. And it would not invoke a dinder on either, yes. Nor would it be like assignment. This is even worse than the use of [] in type hinting which is also using the same sytax for a very different meaning -- at least that one is stil calling __getitem__ :-) -CHB On Sat, Jan 8, 2022 at 8:46 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Sat, Jan 08, 2022 at 06:30:53PM -0800, Ethan Furman wrote:
On 1/8/22 5:46 PM, Steven D'Aprano wrote:
[...] if you hate type annotations because they are unreadable, then you hate Python because Python is unreadable.
Not so.
Are you disputing that annotations use the same syntax as other Python expressions? If not, I don't see how you can deny that "type annotations are unreadable" implies "Python expressions are unreadable", which in turn implies "Python is unreadable".
A simple list comprehension is (usually) quite readable, while a triply-nested list comprehension all on one line is not.
Indeed. We can abuse any syntax. So do we conclude that comprehensions are "unreadable" because we can write obfuscated triply-nested list comprehensions?
Similarly, adding type information in between a variable name and its value is not (for me, and apparently others too) readable.
I think that "unreadable" or "not readable" is a complaint that gets overused, often for the most trivial cases, to the point that it loses all credibility. Especially when it comes from people who are fluent in C (which may not be you, Ethan).
http://unixwiz.net/techtips/reading-cdecl.html
"Easily learned", huh. I think that this is one of the clearest examples of the curse of knowledge as it applies to programming that one could hope to find.
Anyway, let's compare:
# C int n = 44;
# Pascal var n: integer; n := 44;
# Typescript var n: number = 44;
# Java int n = 44;
# Python n: int = 44
There are millions who find the C, Pascal, TypeScript and Java perfectly readable. I don't find it credible that people are incapable of reading the last one.
Aside: such a type hint is redundant, as mypy is perfectly capable of inferring that n = 44 makes n an int. Style guides should recommend against such redundancy, and I would certainly flag that in a code review. A better example of a *useful* type hint would be:
L: list[str] = []
Most horribly of all, cluttering a function header with type information is most unreadable.
I hear you. Adding redundant or unnecessary type hints to parameters just for the sake of having type hints is just clutter, especially if they are never tested by actually running a type checker over the file.
(Untested code is broken code. If not right now, it soon will be.)
Fortunately, we have *gradual typing*, and nobody should be forced to use type hints in their projects if they don't want them. Just as we don't make linters mandatory, we don't make typing mandatory either.
I think that, outside of very simple functions, once we make the decision to annotate a function, we should space them out:
# Better def func(spam: list[str], eggs: float, cheese: str = 'cheddar', aardvark: str|bytes = "", eels: Optional[Tuple[int, str]] = None ) -> Hovercraft:
which makes them much easier to read.
Trying to cram them all into one line is abuse of the syntax every bit as bad as cramming a triply-nested list comp into one line:
# Worse def func(spam: list[str], eggs: float, cheese: str = 'cheddar', aardvark: str|bytes = "", eels: Optional[Tuple[int, str]] = None) -> Hovercraft:
I can read it, I just don't want to. It is too much like hard work compared to the previous layout.
Even if you don't run a type-checker, those annotations can make useful documentation. (At least *sometimes*.) If the parameter name doesn't make it clear what types are allowed, then the annotation can make it clear. So if you don't use a static checker, you can think of type annotations as introspectable documentation.
I started using Python at 2.5. It was simple, clean, and elegant.
And I started using Python at 1.5, when the syntax was even simpler and cleaner. And to this day I will never forget the first time I read Python code, after being told repeatedly how easy to read it, and I couldn't make head or tails of it. All those colons and square brackets, it might as well have been APL. (Not that I knew what APL was back then.)
I knew what a for-loop was, from Pascal, Hypertalk and HP RPN calculators:
# Pascal for i := 0 to 10 do begin block; end;
# Hypertalk repeat with i = 0 to 10 block end repeat
# HP-48 RPN language 0 10 FOR I block NEXT
but I kept seeing loops like this in Python:
for i in range(11):
or worse:
for somename in [stuff, thing, another_thing, widget]:
and worse of all:
for somename in values[1:-1]:
Python for loops looked nothing like any for loop I had seen before, and they freaked me out, and at the time (early 1990s) there was no publicly available internet where I could look anything up or ask for help.
And then there were the brackets. Why does Python sometimes use round brackets, sometimes curly brackets, and sometimes square brackets? x[a] versus x(a)? Why were there sometimes colons inside square brackets and curly brackets {a:b} but never inside round brackets? What was the difference between [1, 2, 3] and (1, 2, 3)?
The whole thing was intimidating, and I just put Python away for about a year and didn't look at it again until I had bought Mark Lutz' "Python Pocket Reference" which helped me make sense of it all. That and his "Learning Python". And never looked back. (Since then, I've often felt that Python has spoiled me from learning other languages.
The point I am making here is not that I was a dimwit who couldn't even read Python, but that "easy to read" and "readable" is more a matter of familiarity than an inherent property of the language itself. With enough familiarity, even APL is easy to read.
If I had stumbled on it at 3.16 with samples, tutorials, and books all infused with typing clutter (which *looks* like boiler-plate even if it isn't) I wouldn't have given it a second glance.
And again, I hear you. I too wish people would tone down their enthusiasm for adding typing to examples that don't need type hints.
We should remember that type hints are a feature aimed at large code bases, where static typing really is valuable. For three line example functions, not so much, not even as documentation.
-- Steve _______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/35G7WXSH... Code of Conduct: http://python.org/psf/codeofconduct/
-- Christopher Barker, PhD (Chris) Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython
On Mon, Jan 10, 2022 at 3:49 AM Christopher Barker <pythonchb@gmail.com> wrote:
If "x->y" is syntactically valid anywhere in Python code, it's not a problem that there are no core data types for which it's meaningful.)
Here's where I'm not so sure -- this looks a lot like a binary operator, but it behaves quite differently. IIUC it would always create a Callable, regardless of what the types were of the two other types. And it would not invoke a dinder on either, yes.
Nor would it be like assignment.
This is even worse than the use of [] in type hinting which is also using the same sytax for a very different meaning -- at least that one is stil calling __getitem__ :-)
From my understanding, "x->y" would create a Callable if given two *types*, but its meaning if given two other objects is still undefined. So there's still room for it to be an operator, just like [] is, and for it to be given semantic meaning for the 'type' type and all of its subclasses. Or alternatively, there's room for it to be given meaning in a completely different way, but still universally (there's a proposal for it to be a form of inline function, although I'm not 100% sure of the details there). ChrisA
El dom, 9 ene 2022 a las 10:50, Chris Angelico (<rosuav@gmail.com>) escribió:
On Mon, Jan 10, 2022 at 3:49 AM Christopher Barker <pythonchb@gmail.com> wrote:
If "x->y" is syntactically valid anywhere in Python code, it's not a problem that there are no core data types for which it's meaningful.)
Here's where I'm not so sure -- this looks a lot like a binary operator, but it behaves quite differently. IIUC it would always create a Callable, regardless of what the types were of the two other types. And it would not invoke a dinder on either, yes.
Nor would it be like assignment.
This is even worse than the use of [] in type hinting which is also using the same sytax for a very different meaning -- at least that one is stil calling __getitem__ :-)
From my understanding, "x->y" would create a Callable if given two *types*, but its meaning if given two other objects is still undefined. So there's still room for it to be an operator, just like [] is, and for it to be given semantic meaning for the 'type' type and all of its subclasses. Or alternatively, there's room for it to be given meaning in a completely different way, but still universally (there's a proposal for it to be a form of inline function, although I'm not 100% sure of the details there).
With the current iteration of PEP 677, `(x) -> y` would return a special object (defined in the `types` module) that simply holds whatever `x` and `y` evaluate to. You could put whatever expression you want at runtime and it would work, though a static checker may be unhappy with you.
ChrisA _______________________________________________ Python-Dev mailing list -- python-dev@python.org To unsubscribe send an email to python-dev-leave@python.org https://mail.python.org/mailman3/lists/python-dev.python.org/ Message archived at https://mail.python.org/archives/list/python-dev@python.org/message/R6OIHWU5... Code of Conduct: http://python.org/psf/codeofconduct/
On Mon, Jan 10, 2022 at 05:39:42AM +1100, Chris Angelico wrote:
From my understanding, "x->y" would create a Callable if given two *types*, but its meaning if given two other objects is still undefined.
The PEP requires parentheses around the argument list, so that would be a SyntaxError. The PEP also states that the arrow syntax would be equivalent to calling Callable. Callable currently enforces that the return type actually is a type, but doesn't check the input types. (I don't know if that is a deliberate design or an oversight.) Assuming that it is an oversight, I would expect that only the following values would be legal for the x and y objects: - a type; - a string (which gets converted to a ForwardRef); - None; and anything else would result in a TypeError. -- Steve
On Mon, Jan 10, 2022 at 12:05 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Mon, Jan 10, 2022 at 05:39:42AM +1100, Chris Angelico wrote:
From my understanding, "x->y" would create a Callable if given two *types*, but its meaning if given two other objects is still undefined.
The PEP requires parentheses around the argument list, so that would be a SyntaxError.
That's a small restriction that makes it less clear as an operator, but sure, pretend I wrote "(x)->y" in the examples.
The PEP also states that the arrow syntax would be equivalent to calling Callable. Callable currently enforces that the return type actually is a type, but doesn't check the input types.
(I don't know if that is a deliberate design or an oversight.)
Assuming that it is an oversight, I would expect that only the following values would be legal for the x and y objects:
- a type; - a string (which gets converted to a ForwardRef); - None;
and anything else would result in a TypeError.
TypeError is absolutely fine; it's still part of the core language syntax.
1 @ 3 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for @: 'int' and 'int'
If I want to create my own class that responds to matmul, I can do that, even though core data types will all TypeError; it's part of the core language and has syntactic, if not semantic, meaning in any context. ChrisA
On Sun, Jan 09, 2022 at 08:42:14AM -0800, Christopher Barker wrote:
Perhaps it's worth remembering that this thread spun off one about adding syntax to Python because the current syntax isn't capable of easily expressing an important type hinting concept (i.e. Callable).
I shall quote the PEP's justification (in part) for the benefit of those who haven't read it. [quote PEP 677] There are a few usability challenges with Callable we can see here: • It is verbose, particularly for more complex function signatures. • It relies on two levels of nested brackets, unlike any other generic type. This can be especially hard to read when some of the type parameters are themselves generic types. • The bracket structure is not visually similar to how function signatures are written. • It requires an explicit import, unlike many of the other most common types like list. Possibly as a result, programmers often fail to write complete Callable types. Such untyped or partially-typed callable types do not check the parameter types or return types of the given callable and thus negate the benefits of static typing. [/quote] These are legitimate issues with the Callable type hint. Does that mean that the existing syntax "isn't capable of easily" expressing callables? I don't think so. "Easily" is subjective, and at the risk of undermining the justification for the PEP, I think it is fairly easy: # type hint for a function of two parameters, the first takes # an int, float or complex, the second a bool, and returns # either a str or bytes. from typing import Callable T = Callable[[int|float|complex, bool], str|bytes] It was harder to write it out in English than to write the type hint. So its not *difficult*, there's one import and one class with a subscript needed. The type system is perfectly capable of expressing that. It is no more hard to read than any other expression of equivalent complexity: from aardvark import Surveying T = Surveying((spam+eggs+cheese, eels), foo*bar) But can we do better? The PEP authors think we can, and I am inclined to agree with them. While it is true that "not everything needs to be a builtin", in the case of typing, we are evolving to use builtins where possible: * we use int, and always have, not typing.Int; * what was once typing.List[int] is now list[int]; * what was once Union[float, int] is now float|int etc, but Callable is the last remaining common compound type without a syntactic shortcut. Its not that declaring callables are *too hard* (let alone impossible) but that we can do better than what we've got. Callable[[int|float|complex, bool], str|bytes] (int|float|complex, bool) -> str|bytes The second looks like executable pseudo-code for a function declaration. It avoids the import and the nested square brackets, and it is shorter without being so terse it becomes cryptic. The improved syntax looks just like a function signature with the parameter names excised: def func(a: int|float|complex, b: bool) -> str|bytes which I think is a win.
So arguing that Python is completely readable for type hints is a bit off-mark, isn't it?
What does "completely readable" mean? If you mean that it is impossible to compose a type-hint of such stupendous complexity and obfuscatory brilliance that even the infamous RFC-822 email address validator regex appears trivial in comparison? http://www.ex-parrot.com/~pdw/Mail-RFC822-Address.html Of course I don't mean that. Type hints are expressions, and expressions can have arbitrary complexity, including "too damn high". I argued that type hints are no harder to read than other expressions of the equivalent complexity. I didn't argue that they can't be abused or that the syntax cannot be improved. More to follow... -- Steve
TL;DR: declaration only syntax is a non-starter. Even if we tried to add specialised syntax that only appears in annotations, it would become a regular Python expression almost immediately. On Sun, Jan 09, 2022 at 08:42:14AM -0800, Christopher Barker wrote:
Is it more clear for readers to have two different (but related) related syntaxes for two different (but related) purposes, or to have one Sytax that is used in different ways?
That is not a valid "either/or" choice. The choice is: * Python expressions that have multiple meanings (the status quo); (e.g. subscripting `obj[x]` already had 4+ meanings before its use in typing) * or Python expressions that have multiple meanings, PLUS new declaration syntax that is only valid in annotations. And I will argue below that the culture of Python is such that the second choice is practically *impossible* without a radical shift in the culture. Let's say that we decide to add specialist declaration-only syntax for type-hints. What next? First, we need a way to distinguish between type-hints that are declarations from type-hints that are expressions. Let's say that we use the proposed t-string syntax: spam: t"declaration goes here" That tells the interpreter to treat everything inside the quotes as a pure declaration, not an expression, with no runtime effect. I'm going to put aside the question of what syntax is allowed for the declarations inside the quotes, because that's actually not that important. What happens after we add this declaration syntax? The first thing that happens is that people interested in runtime processing of annotations will want to inspect the declaration, without having to scan the source code (which might not be available). Python has a strong culture of runtime introspection, starting with dir() way back in Python 1.x days, so if you think the core devs are going to resist the call to make those annotations visible at runtime, I think you are remarkably ~~wrong~~ optimistic :-) That means that the declaration has to have a runtime effect. Something has to go into the `__annotations__` dict: __annotations__['spam'] = ... # what goes here? (It doesn't matter precisely what the value that goes into the mapping is, it could be a string, or a type object, whatever it is, it is a value.) So our declaration is no longer a declaration, like `global`, it is an expression that returns a value. The parser merely restricts its use to annotations like `spam: t"declaration"`. Why can't we use that nifty new declaration syntax in type aliases? I mean, it already evaluates to a value that can be inspected at runtime, so we just have to relax the restriction on where it can appear so we can use it in type aliases as well as directly in annotations: T = t"declaration goes here" spam: T|None But if we can do that, then our declaration is just another expression. It has a value, it can appear outside of annotations. People are going to want to treat it as a first-class value (no pun intended), even if only for testing purposes: # Unit tests for T in [t"declaration", t"another declaration", t"yet another", t"my hovercraft is full of eels"]: self.assertIsInstance(T, typing.Generic) ... Even if it doesn't happen from day 1, rapidly whatever restrictions on where the syntax is allowed will be relaxed, because that's the sort of language Python is. Once we make that first (and inevitable!) step to give these type declarations an introspectable value, then they will become first-class expressions as sure as death and taxes. So I maintain that *declaration only syntax* is doomed. Whatever syntax we invent, there is going to be irresistable pressure to make it a first-class expression and not just restrict it to annotations. And if that is the case, then why do we need the t-string quotes? That's not a rhetorical question. Something like t-string quotes, or some other pair of delimiters, *may* be useful if we have our hearts set on syntax which is radically different from regular Python expressions: spam: t"array 1 to 50 of int" # Hybrid of Pascal and Hyperscript eggs: t"{(+⌿⍵)÷≢⍵}" # I don't even... Just as we have `[ ... ]` delimiters for list comprehensions. Maybe it turns out that there are cases where we need delimiters. But if so, I hope they aren't quotation marks (with or without the t-prefix), since that will be confusing as far as the existing use of strings as forward references. But why do we want type hints to be so radically different from regular Python expressions? How does that make typing easier to read and the language less complicated, if we have to learn *two* languages to be fluent in Python instead of one? Outside of that, the t-string delimiters are redundant. There is no need to wrap the proposed arrow syntax in quotes, as the PEP makes clear they can be handled just fine as an expression. The arrow expression doesn't need extra delimiters, it adds nothing. They're just noise.
If "x->y" is syntactically valid anywhere in Python code, it's not a problem that there are no core data types for which it's meaningful.)
Here's where I'm not so sure -- this looks a lot like a binary operator, but it behaves quite differently.
How is it different? Aside from the syntax requirement that the left operand is parenthesized, we can treat it as an operator: (one of more input args) arrow operator return-arg Whether the parser treats it as a binary operator or something different, like the dot name.expression, isn't really important. It is a symbol that has a left-operand and a right-operand, using infix syntax. I call that an operator, at least in informal language. There's no dunder for the arrow, just like `is`, `is not`, `or`, `and`, `not`, so no operator overloading. Just like `is` etc. They are still operators. And just like other operators, if you pass the wrong input types, it will raise a TypeError. -- Steve
So if you hate type annotations because they are unreadable, then you hate Python because Python is unreadable.
That seems rather harsh. I suspect if those of us who are uncomfortable with the typing subsystem actually hated Python we would have found our way to the exits long ago. Typing was always supposed to be optional, so I didn't worry too much about it at the time. As Jack indicated though, while it may be optional at the language level, it's often not truly optional at the organizational level. As you indicated, there are two things going on, Python syntax and the semantics which go along with it. Python's economical syntax is a terrific reflection of its runtime semantics, hence the use of the phrase "executable pseudocode" to describe Python (at least in the past). Just because you are using Python syntax for your declarations doesn't mean that (a) mapping the semantics of the desired declarations onto existing syntax will be straightforward or (b) that the semantics of those declarations will be reflected as effortlessly as it reflects runtime semantics. Skip
On Sat, Jan 08, 2022 at 08:36:57PM -0600, Skip Montanaro wrote:
So if you hate type annotations because they are unreadable, then you hate Python because Python is unreadable.
That seems rather harsh. I suspect if those of us who are uncomfortable with the typing subsystem actually hated Python we would have found our way to the exits long ago.
Right. That's my point, or at least part of it. I've read heaps of complicated, confusing, even outright obfuscated Python expressions. Some of it even written by me, that a couple of months later I couldn't work out what I had done. So did I conclude that "Python expressions are unreadable"? No I did not. When I read an obfuscated, confusing, complex type hint, do I decide that all type hints are "unreadable"? No I don't do that either. Here is the type hint for `len`, taken from the stub file in typeshed: def len(__obj: Sized) -> int: ... Putting the mysterious double underscore naming convention aside, I do not find it credible that anyone capable of programming Python beyond a beginner level can find that "unreadable". Not by any definition of unreadable I can think of. Even if you can't guess what "Sized" means, it isn't that hard to track it down: from typing import Sized # among many others and from there to collections.abc. This is not brain surgery folks :-) https://www.youtube.com/watch?v=THNPmhBl-8I We should not dismiss all of typing as "unreadable". Type hints are just expressions. If you can read Python expressions, you can read type hints. Some are simple, some are complex, some are confusing. We should do our best to improve the complex and confusing ones, using all the tools at our disposal: * named type aliases; * style guides for laying out the annotations to make them physically easier to read (one parameter per line in complex function signatures works for me); * improving the `typing` module DSL; * perhaps even adding new syntax like the arrow syntax; but we shouldn't dismiss the whole thing as "unreadable" if what we actually mean is "its unfamiliar and I don't like it".
Typing was always supposed to be optional,
And it still is. Alas, we can't do anything about third-party projects mandating type hints (maybe they have a good reason for mandating them!). But perhaps we can help discourage some of the excessive zeal for annotating everything in sight.
so I didn't worry too much about it at the time. As Jack indicated though, while it may be optional at the language level, it's often not truly optional at the organizational level.
We can't prevent organisations and third-parties mandating the use of linters, or IDEs, or particular naming conventions, or any other style convention they want. PEP 8 zealotry is especially prevalent out in the world. I've come across people with their own idiosyncratic style, like "Never use comprehensions, only for-loops", and others who insist "Never use for-loops, only comprehensions". What are we going to do, dismiss comprehensions as a bad idea because some people are irrationally pro- or anti-comprehensions? I don't think so.
As you indicated, there are two things going on, Python syntax and the semantics which go along with it. Python's economical syntax is a terrific reflection of its runtime semantics, hence the use of the phrase "executable pseudocode" to describe Python (at least in the past).
Right. Beyond the easy cases, typing is hard. It is often easier to write code that works for typical data you care about, than to convince the type-checker that the code works :-) I don't know if this applies to gradual typing, but I imagine it probably does. Type checking is a hard problem, and if your type system is powerful enough to use, it is undecidable: http://composition.al/blog/2017/02/27/why-does-a-turing-complete-type-system... https://forums.swift.org/t/swift-type-checking-is-undecidable/39024 Even if your type system is not Turing complete, it is still going to be pretty powerful. We're not using Pascal any more :-) And that means that the types themselves are communicating some fairly complex semantics. Blaming the syntax for something which is inherently hard is not helpful.
Just because you are using Python syntax for your declarations doesn't mean that (a) mapping the semantics of the desired declarations onto existing syntax will be straightforward or (b) that the semantics of those declarations will be reflected as effortlessly as it reflects runtime semantics.
Indeed. And we can say the same thing about using Python syntax as code. Mapping the semantics of your desired behaviour into syntax is not always straightforward, nor is reading the code and inferring the semantics. If it were, anyone could be a rockstar ninja coder, and programming would be a minimum wage job. We accept without blinking comprehensions, decorators, descriptors, metaclasses, multiple inheritence, horrifically complex class hierarchies, abstraction on top of abstraction, design patterns, metaprogramming, threading, async programming and more, all of which can at times be every bit as complex as typing. I've even had the pleasure of trying to understand Continuation Passing Style in Python. The code clearly worked, but I'm damned if I could understand how. As programmers, there is a ton of complicated stuff we have to deal with. So why the hate towards type hints? -- Steve
Here is the type hint for `len`, taken from the stub file in typeshed:
def len(__obj: Sized) -> int: ...
Putting the mysterious double underscore naming convention aside, I do not find it credible that anyone capable of programming Python beyond a beginner level can find that "unreadable". Not by any definition of unreadable I can think of.
Sure, that's pretty trivial, no question. As would be the similar C declaration. As Glenn Lindermann reminded me of cdecl: https://cdecl.org/ you can see how you can get carried away. It's the "getting carried away" parts of the (sometimes organizationally mandatory) type system in Python that are problematic for me, not the simple sized object input, int output sort of thing. You have people asking questions like these: https://discuss.python.org/t/contravariant-typing-type/12741 https://discuss.python.org/t/how-to-annotate-a-new-dict-class-with-typeddict... I don't know if they are just trying to run with scissors, are way the hell off in the weeds, or if the more esoteric corners of tle typing world are simply going to continue to impose themselves on the rest of us. There will always be people who want to express "declare foo as pointer to function (void) returning pointer to array 3 of int" (from the cdecl.org website). Other people have to read that. Maybe Python will eventually grow a pydecl.org domain and website to serve a similar purpose. :-)
Even if your type system is not Turing complete, it is still going to be pretty powerful. We're not using Pascal any more :-) And that means that the types themselves are communicating some fairly complex semantics.
Blaming the syntax for something which is inherently hard is not helpful.
I don't think anyone's blaming the syntax. I interpreted Jack's suggestion to mean that we would be able to do better with t-strings encapsulating a little language designed to cleanly describe types. I first encountered Python in the 1993-1994 timeframe (1.0.something). Part of its appeal to me at least (and to many others I think) was that it was the anti-Perl. Perl's obfuscation wasn't in its typing. It was elsewhere (everywhere else?). With a full-fledged type system in place it seems like Python is starting to desert that niche. (Yes, I realize Perl is no longer the big dog it once was.) Skip
On 9/01/22 2:46 pm, Steven D'Aprano wrote:
if you can read expressions:
spam(eggs | cheese, aardvark)
then you can read type expressions:
Spam[Eggs | Cheese, Aardvark]
That's like me saying that I can read Greek just because I know how to pronounce the letters. I do, but that doesn't mean I know what any of it means! Most of the effort of learning to read type expressions, in any language, is learning their semantics, which is very different from value expressions. Whether they happen to superficially resemble other parts of the language is pretty much irrelevant to me. -- Greg
On Mon, 10 Jan 2022 at 23:04, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Most of the effort of learning to read type expressions, in any language, is learning their semantics, which is very different from value expressions. Whether they happen to superficially resemble other parts of the language is pretty much irrelevant to me.
While this is true, what *is* a problem with having a different language, is that you have two very similar syntactic constructs ("expression" and "type expression") and you have to learn which can be used where. And that's actually not as trivial as you'd think at first. The typing.cast function takes a type expression as its first parameter. If type expressions are not expressions, syntactically, then that means that typing.cast(MyType, obj) is *not* syntactically a function call. So what is it? Say it's a new syntactic element, a "cast expression". Then the syntax for an expression must be extended to include cast expressions. And what *is* the syntax of a cast expression? "cast_literal(type_expr, expr)"? Then what's "cast_literal"? The value "typing.cast"? What about "from typing import cast", or "import typing as t" with "t.cast", or "from typing import cast as typecast", or even "my_cast = getattr(typing, "cast")? Suddenly there are a huge number of unanswered questions to be considered, and the answers to those questions need to be learned by people. Paul. PS Having said that, existing type checkers may or may not recognise all of the possible ways to call the cast function that I described. Do we want to mandate what type checkers do as part of the language definition? Do we want to restrict the language definition based on what 3rd party type checkers can achieve?
Barry:
The advantage to users of keeping the languages the same is that readers of your code don’t have to learn two disparate syntaxes to make sense of what they’re reading. One of Python’s enduring strengths has been its readability.
Agreed. But if the little language is (a) clearly flagged and (b) has a different domain I think this is much less of a problem. I don’t think f-strings are seen as a problem, far from it, because they’re clearly flagged. That’s why I suggested t-strings. And while from a Python parser point-of-view the grammar of current type expressions are the same as the grammar of other Python code I think that for human readers this is not the case: there’s a lot of square brackets but nothing is being indexed, to name one major readability issue... Stéfane:
Using the same syntax may have some benefits for language implementors (e.g. less complex grammar to implement), but I don’t really see these benefits for language users.
As an example, and I don’t know if this has been discussed before, I think a pretty neat syntax construct for optional argument would be (like, for instance, in Kotlin):
def f(x: int? = None): ...
I introduced the t-strings specifically because I think it would be beneficial to have the little language a clearly flagged and have as little interaction with “normal” Python as possible. Your example here works fine for Optional, and looks pretty understandable if you know C# (or apparently Kotlin), but it doesn’t solve all the other problems with readability. And I think you’ll quickly run out of constructs once you try to fix more problems if you don’t want to clash with existing Python syntax… -- Jack Jansen, <Jack.Jansen@cwi.nl>, http://www.cwi.nl/~jack If I can't dance I don't want to be part of your revolution -- Emma Goldman
On Sun, Jan 09, 2022 at 02:20:45AM +0100, jack.jansen@cwi.nl wrote:
Barry:
The advantage to users of keeping the languages the same is that readers of your code don’t have to learn two disparate syntaxes to make sense of what they’re reading. One of Python’s enduring strengths has been its readability.
Agreed. But if the little language is (a) clearly flagged and (b) has a different domain I think this is much less of a problem.
I disagree.
I don’t think f-strings are seen as a problem, far from it, because they’re clearly flagged.
On the contrary, f-strings are not really a second language. f-strings involve *ordinary Python expressions* plus a set of formatting codes which are mostly similar or identical to those used by the format method and string interpolation with the `%` operator.
That’s why I suggested t-strings. And while from a Python parser point-of-view the grammar of current type expressions are the same as the grammar of other Python code I think that for human readers this is not the case: there’s a lot of square brackets but nothing is being indexed, to name one major readability issue...
Nothing is being *indexed* here either: mydict[key] I don't think that Mathematica code is harder to read because it uses square brackets for function calls instead of round brackets. I challenge you to say that you cannot read these: Range[10] FindShortestPath[graph, start_vertex, target_vertext] Even when Mathematica uses syntax that is unfamiliar, I expect that you would be able to guess what this does: StringReplace["abbaabbaa", "ab" -> "X"] and if you can't, it's not because of the square brackets. As I mentioned in a previous email, I can see a number of reasons why typing is hard, but the syntax (ordinary Python expressions) is not why it is hard. We can, I think, improve elements of the typing DSL. `T|S` is, I think, an improvement over `Union[T, S]` for the same reason that `a|b` would be an improvement over `bitwise_and(a, b)` or `set_intersection(a, b)`. Likewise, I think that we should accept PEP 677 (arrow notation as sugar for Callable). But I wouldn't want to see type hints diverge into a completely different language from Python.
I introduced the t-strings specifically because I think it would be beneficial to have the little language a clearly flagged and have as little interaction with “normal” Python as possible.
And that is exactly why I think that it is not a good idea. Having type hints use regular Python syntax is not a design flaw to be fixed. -- Steve
On Sat, Jan 08, 2022 at 12:59:38AM +0100, jack.jansen@cwi.nl wrote:
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored. Possibly because it’s a really stupid idea, but let me post it again on the off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language for type specifications?
Any time we are tempted to prefix a question with "Why not ...", the question is backwards. The right question is, "why should we ...". Python is 30 years old and mature, with a HUGE ecosystem of users, libraries, tooling, etc. It is far, far easier to get changes wrong than to get them right, which is why we should be conservative when it comes to language changes (including syntax). Changes to the language are "default deny", not "default accept", and it is up to the proponents of the change to prove their case, not for opponents to disprove it. https://www.curiousefficiency.org/posts/2011/02/status-quo-wins-stalemate.ht... https://www.curiousefficiency.org/posts/2011/04/musings-on-culture-of-python...
If you look at other programming languages you’ll see that the “type definition sub-language” is often completely different from the “execution sub-language” [...]
Yes, and that makes other programming languages harder to use and harder to learn. Even Pascal, which has a nice human-readable declaration syntax, requires you to learn more before you can do anything. C is particularly bad, since there are effectively three distinct languages to learn: macros, type declarations, and C code. Anyone who has learned enough C to get good at it is likely to under- estimate how hard C is to learn, due to survivorship bias (those who have survived their apprenticeship as a newbie C coder to become an expert are not representative of all newbie C coders) and the curse of knowledge (experts often find it hard to put themselves in the shoes of non-experts). So I think that we should avoid the temptation to have a distinct language for type annotations. I think that Guido's initial reasoning, that type annotations are Python expressions, rather than distinct syntax for declarations, was correct, and I think that the Steering Council is right to insist on keeping annotations aligned as Python code. Mind you, that doesn't necessarily mean that we cannot introduce new syntax for use in annotations. (Just as we added three-argument slicing and Ellipsis specifically for use in Numpy.) So long as they are usable outside of annotations, they remain "part of the Python language" rather than a distinct "type declaration syntax". For example, the arrow syntax for Callable `(int) -> str` (if accepted) could be a plain old Python expression, usable anywhere the plain old Python expression `Callable[[int], str]` would be.
Python typing uses basically a subset of the execution expression syntax as its declarative typing language.
I think a good way of putting it is that the typing module defines a little DSL (Domain Specific Language) for type hints, but annotations themselves are just Python expressions.
What if we created a little language that is clearly flagged, for example as t”….” or t’….’? Then we could simply define the typestring language to be readable, so you could indeed say t”(int, str) -> bool”. And we could even allow escapes (similar to f-strings) so that the previous expression could also be specified, if you really wanted to, as t”{typing.Callable[[int, str], bool}”.
The following are not rhetorical questions. I don't know the answers, which is why I am asking. 1. Are these t-strings only usable inside annotations, or are they expressions that are usable everywhere? 2. If only usable inside annotations, why bother with the extra prefix t" and suffix "? What benefit do they give versus just the rule "annotations use this syntax"? 3. If usable outside of annotations, what runtime effect do they have? The t-string must evaluate to an object. What object? 4. If the syntax allowed inside the t-string is specified as part of the Python language definition, why do we need the prefix and suffix? E.g. if we can write: # Equivalent to T = Callable[[int], str] T = t"(int) -> str" and have the arrow syntax defined by the language, then surely the prefix and suffix is redundant. Likewise, if this is allowed: def func(arr: t"array [1...10] of int") -> str: ... why not just allow this? def func(arr: array [1...10] of int) -> str: ... 5. What difference, if any, is there between `t"{expression}"` and `expression`? If there is no difference, then I don't think that the t-string syntax adds anything to this proposal. Remove the t-string syntax, and just write the type expression. This is not the case with f-strings, where they actually do add something to the code: `f"{expr}"` is not the same as `expr`. -- Steve
This really needs a new thread, maybe even a new PEP, but ... On Sat, Jan 8, 2022 at 5:27 PM Steven D'Aprano <steve@pearwood.info> wrote:
If I can make a wild suggestion: why not create a little language for type specifications?
Any time we are tempted to prefix a question with "Why not ...", the question is backwards. The right question is, "why should we ...".
OK -- we should create a little language for type specifications because it's proving to be really difficult to cleanly express types with Python's current allowable syntax. This is documented by the PEP that started this thread -- if you have to expand the syntax of Python to make clean expression of typing possible, it's time to make a new language., rather than complicating Python.
... we should be conservative when it comes to language changes (including syntax).
exactly -- we are now faced with changing language syntax to satisfy the needs of one particular optional corner of Python -- we should be particularly very careful about that! (and it's not just callables -- there was a long thread in python-ideas about a yeear (?) ago about extending indexing syntax, that started with the needs of typing.
If you look at other programming languages you’ll see that the “type
definition sub-language” is often completely different from the “execution sub-language”
Yes, and that makes other programming languages harder to use and harder to learn.
I would argue that hat's because you have to learn type specifications at all -- not because they have different syntax. IN fact, in my very limited C experience I found it very confusing that often declare a function was the same as defining it -- which is this bit of code doing? In practice the declarations are usually in a header file, but that's not (I think) required by the language. IT would be less confusing if the type declaration were clearly different. But anyway, right now in PYthon, type declarations use standard Pyhton syntax, but they use it in a non-convensional way. Is it more or less confusing that: list[int] means "a list full of integers" and a_list[an_int] means return the value (which could be any type) of the item at index an_int? Does the same syntax with very different meanings make it easier or harder to learn??
For example, the arrow syntax for Callable `(int) -> str` (if accepted) could be a plain old Python expression, usable anywhere the plain old Python expression `Callable[[int], str]` would be.
but what would `` -> `` mean ?!?! if all it meant was create an instance of the Callable class, then you have a typing mini-language that can be used anywhere -- more or less confusing ?!? Or it could be used as a new way to spell lambda -- I know a lot of folks would like a cleaner way to spell lambda, but would that really make the language simpler? I think a good way of putting it is that the typing module defines a
little DSL (Domain Specific Language) for type hints, but annotations themselves are just Python expressions.
OK -- and if you had t" " -- then that would be a Python expression, too :-)
What if we created a little language that is clearly flagged, for example as t”….” or t’….’? Then we could simply define the typestring language to be readable, so you could indeed say t”(int, str) -> bool”. And we could even allow escapes (similar to f-strings) so that the previous expression could also be specified, if you really wanted to, as t”{typing.Callable[[int, str], bool}”.
The following are not rhetorical questions. I don't know the answers, which is why I am asking.
1. Are these t-strings only usable inside annotations, or are they expressions that are usable everywhere?
as you say -- not much point if they aren't usable anywhere -- why not?
2. If only usable inside annotations, why bother with the extra prefix t" and suffix "? What benefit do they give versus just the rule "annotations use this syntax"?
Agreed - -though I think there is a certain clarity about it -- also, it would make a the transition a bit easier if the idea was adopted.
3. If usable outside of annotations, what runtime effect do they have? The t-string must evaluate to an object. What object?
I'm just spit-balling here, but maybe an "Annotation Object" -- which could be a pre-compiled or normalized version of the annotation string. kind of like a pre-compiled regex, maybe? I have not idea if MyPy and friends have sucha. normalized version, but it does seem like it could be handy.
4. If the syntax allowed inside the t-string is specified as part of the Python language definition, why do we need the prefix and suffix?
E.g. if we can write:
# Equivalent to T = Callable[[int], str] T = t"(int) -> str"
and have the arrow syntax defined by the language, then surely the prefix and suffix is redundant.
I think the idea is that , e.g. -> wouldn't be allowed outside an t-string and other things, like [] might mean sometihng different. But in the end, if they are not going to be pre-processed, anon only allowed in annotations, then yes, no nead for the t"" at all. I think the first question is whether a typing language is a good idea at all -- then we can worry about these details. This is not the case with f-strings, where they actually do add
something to the code: `f"{expr}"` is not the same as `expr`.
right, and f-strings ultimately evaluate to normal strings as well. -CHB -- Christopher Barker, PhD (Chris) Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython
On 9 Jan 2022 at 02:22:31, Steven D'Aprano <steve@pearwood.info> wrote:
On Sat, Jan 08, 2022 at 12:59:38AM +0100, jack.jansen@cwi.nl wrote:
I posted this suggestion earlier in the callable type syntax discussion, at which point it was completely ignored. Possibly because it’s a really stupid idea, but let me post it again on the off chance that it isn’t a stupid idea but was overlooked.
If I can make a wild suggestion: why not create a little language
for type specifications?
Any time we are tempted to prefix a question with "Why not ...", the question is backwards. The right question is, "why should we ...".
Python is 30 years old and mature, with a HUGE ecosystem of users, libraries, tooling, etc. It is far, far easier to get changes wrong than to get them right, which is why we should be conservative when it comes to language changes (including syntax). Changes to the language are "default deny", not "default accept", and it is up to the proponents of the change to prove their case, not for opponents to disprove it.
I agree 100% on these principles. As a counter-argument, in this specific case, I’d say that the Python typing ecosystem is only ~5 years old and still evolving rapidly, so these principles apply with less force. I personally believe that the syntax for type annotation doesn’t have, from the point of view of users, to be strictly Python, as long as it’s pythonic. I can sympathise with the idea of repurposing the regular Python syntax to describe types, and agree that it makes easier to bootstrap tools (typecheckers, IDEs…) but I think it has its limits in terms of expressivity, first. Second, I think willing to keep the same syntax constructs for the sake of lowering the cognitive burden of new developers is a red herring. These are two different languages, with entirely different semantics, and the links between them is not complement intuitive. As an example (just for the sake of argument, I’m not proposing this kind of change) one could argue that 'list[int]’ doesn’t really relate to how lists are constructed or how the bracket operator is used in regular Python. A more intuitive construct (from this point of view) could have been ‘[int…]’. Same for dictionaries: ‘dict[int, str]’ vs. ‘{int: str}’ which looks closer that how dictionaries are constructed in Python. S. -- Stefane Fermigier - http://fermigier.com/ - http://twitter.com/sfermigier - http://linkedin.com/in/sfermigier Founder & CEO, Abilian - Enterprise Social Software - http://www.abilian.com/ Co-Founder & Co-Chairman, National Council for Free & Open Source Software (CNLL) - http://cnll.fr/ Co-Founder & Chairman, Association Professionnelle Européenne du Logiciel Libre (APELL) - https://www.apell.info/ Co-Founder & Spokesperson, European Cloud Industrial Alliance (EUCLIDIA) - https://www.euclidia.eu/ Founder, PyParis & PyData Paris - http://pyparis.org/ & http://pydata.fr/
Steven D'Aprano wrote:
On Sat, Jan 08, 2022 at 12:59:38AM +0100, jack.jansen@cwi.nl wrote:
For example, the arrow syntax for Callable `(int) -> str` (if accepted) could be a plain old Python expression, usable anywhere the plain old Python expression `Callable[[int], str]` would be.
In principle, yes. In practice, I think the precedence of "->" might be tricky, particularly if the (int) part discourages people from wrapping the full expression in parentheses.
What if we created a little language that is clearly flagged, for example as t”….” or t’….’? Then we could simply define the typestring language to be readable, so you could indeed say t”(int, str) -> bool”. And we could even allow escapes (similar to f-strings) so that the previous expression could also be specified, if you really wanted to, as t”{typing.Callable[[int, str], bool}”.
The following are not rhetorical questions. I don't know the answers, which is why I am asking.
1. Are these t-strings only usable inside annotations, or are they expressions that are usable everywhere?
I assume they *could* be used anywhere, there just wouldn't be huge reasons to do so. Sort of like a string expression can be an entire statement; there just usually isn't much reason (except as a docstring) to do it.
2. If only usable inside annotations, why bother with the extra prefix t" and suffix "? What benefit do they give versus just the rule "annotations use this syntax"?
It provides a useful box around the typing, so that people who are not currently worried about typing can more easily concentrate on the portion they do currently care about.
3. If usable outside of annotations, what runtime effect do they have?
They create a string. Which may or may not be a useful thing to do.
The t-string must evaluate to an object. What object?
A string. The various "let us delay annotation evaluation" proposals have made it clear that the people actually using typing don't want it to slow things down for extra evaluation until they explicitly call for that evaluation, perhaps as part of a special run.
4. If the syntax allowed inside the t-string is specified as part of the Python language definition, why do we need the prefix and suffix?
Same answer as number 2 ... it allows typing to be a less intrusive neighbor. I don't think t" " is as good as some sort of braces, but ... we're out of conventional braces available in ASCII.
Likewise, if this is allowed: def func(arr: t"array [1...10] of int") -> str: ...
How many arguments do I pass to func? That is already tricky to see at a glance, but
def func(arr: array [1...10] of int) -> str: ...
is even more difficult to parse. By the time I've mentally attached the "of" and "int" to the indexed (but not really) array that just describes a type, I've forgotten what I was looking for and why.
5. What difference, if any, is there between `t"{expression}"` and `expression`?
In addition to the box (so readers can more easily filter it out), there is also a flag to typing systems saying that they *should* elaborate the string. What they elaborate it into will be very different from a regular string. That won't happen every time the module is imported, but it will happen when the string is actually needed for something. -jJ
participants (15)
-
Barry Warsaw
-
Chris Angelico
-
Christopher Barker
-
Ethan Furman
-
Glenn Linderman
-
Greg Ewing
-
h.vetinari@gmx.com
-
jack.jansen@cwi.nl
-
Jelle Zijlstra
-
Jim J. Jewett
-
Paul Moore
-
Serhiy Storchaka
-
Skip Montanaro
-
Steven D'Aprano
-
Stéfane Fermigier