A comment by EthanHS inspired me to write this:
From the perspective of a "long time" end-user of Python annotations:
in many discussions at PyCon, I have heard that people do not like the current state of forward declarations... I think that not requiring the string syntax is needed... the argument could be made that (annotations) are different enough to merit a change (to the language)...
Awesome to see i'm not the only one. In our software engineering team we have been using annotations in a 100k line code base for over 2 years (not google scale but not toy either), as comments-on-steroids: they are readable by developers to guide them in how to use a function, and by computers to provide better code completion and refactoring which in turn increases developer productivity and decreases the incidence of regressions. We love them for that. But we have refused to use strings as annotations, they do not fit and do not flow as other Python code does. I wrote about this in on reddit, and others have written about it there too.
One thing that sets Python apart from so many other languages is how well its syntax matches the way developers think about programs. With that in mind, it is indeed reasonable for Developers to expect the following to work:
class Foo:
def method(self, foo: Foo) -> Foo:
...
and be very disappointed to find out it doesn't because of deep-down machinery that most of them do not care about to get their daily job done.
Sorry, this reply has gotten way longer than I expected, so if you think this github issue is not the right place, I'll be happy to move it, maybe to reddit
Rather than making all annotations be strings, annotations should become first class citizens in Python (in other words type(Either[int, str])
would print <annotation ...>
and woud be treated as their own type of object at parse time, compile time, run time, whatever time). Take this bit of code:
T = TypeVar('T', int, str)
def func(bar: T, foo: T) -> Dict[str, T]:
...
This is the equivalent of writing assembly in a C++ program: it is neither concise nor explicit, i.e. it clashes entirely with Python. BTW I'm not complaining here, I understand that type hinting is a tough problem, even more so to retro-fit into a mature language, that it is "work in progress", that some things just have to be tried, and trust me I really wish I had enough brains to modify the interpreter myself and test out the ideas.
In any case, my point in the last paragraph is that TypeVar should be "an assembly level" instruction" that gets used behind the scenes (if at all), so that we could write the following:
def func(bar: Either[int, str], foo: TypeOf(bar)) -> Dict[str, TypeOf[foo]]:
...
meaning that the type of foo is expected to be the same as the type of bar at time of call, so if bar is is an int, then foo should be an int too. Also that the values in returned map are of the same type as foo at call time. The objective being that func('foo', 'foo')['baz'].
would show methods of str whereas func(123, 456)['baz'].
would show methods of int. It is more verbose, but far more concrete.
Also consider that as type hinting becomes more popular, the scope of its use is going to broaden to express more complicated relationships. But we shouldn't be trying to cram tons of information in the parentheses of a function call; you want to have this information close by (not in a separate file), you want to have it "in function scope" (not in namespace outside of function), and you want some room for the language to grow this capability organically. So although the above example (or something equally natural) could be made into valid Python, it could also be ok to support only basic type hints in the parentheses and return type, and "full-fledged" type hinting in a separate "section" of a function. It could look like this:
class Foo:
def func(self, bar, foo): # full annotations next section:
:bar: Either[int, str]
:foo: Tuple[Foo, TypeOf(bar)] # note Foo can be in there
:return: Dict[str, TypeOf[foo]]
"""
docstring
"""
...
def func2(self, bar: int, foo: List[int]): # simple annotations
...
The last thing that might still be in scope in the context of annotations as strings vs first-class objects, is that not all types are annotated the same way. Indeed this is a bit confusing: you annotate with str for strings but with List for lists. But if annotation objects were first class citizens, this could be fine: there could be a class called 'str', and there could be an annotation object called 'str'; by default the langue gives the two "things" the same name. How do you distinguish between the two? May the colon operator since it is already prominent in type hinting: :str
could be an annotation object for the str
class; :List
would the annotation object for the list class; etc. By default every type would have a corresponding annotation object of the same name in the language, and only (for example) if the type overrides some special member, like annotation_type, does it use a different instance of Annotation than the default. So str would not override this, but list would. In this case the last example could become this:
class Foo:
def func(self, bar, foo, baz): # full annotations next section:
bar is :Either[int, str]
foo is :Tuple[Foo, TypeOf(bar)] # note Foo can be in there
baz is :int
return is :Dict[str, TypeOf[foo]]
"""
docstring
"""
...
def func2(self, bar :int, foo :List[int]=None): # simple annotations
...
Well, it's probably too late to introduce this into the language but maybe something similar.
Sorry if this is not the right place to post this, I'd be happy to move it out of this thread if it is out of line.