[Python-Dev] PEP 563: Postponed Evaluation of Annotations

Jukka Lehtosalo jlehtosalo at gmail.com
Thu Nov 2 14:39:59 EDT 2017


On Thu, Nov 2, 2017 at 3:45 PM, Steven D'Aprano <steve at pearwood.info> wrote:

> On Wed, Nov 01, 2017 at 03:48:00PM -0700, Lukasz Langa wrote:
>
> > This PEP proposes changing function annotations and variable annotations
> > so that they are no longer evaluated at function definition time.
> > Instead, they are preserved in ``__annotations__`` in string form.
>
> This means that now *all* annotations, not just forward references, are
> no longer validated at runtime and will allow arbitrary typos and
> errors:
>
> def spam(n:itn):  # now valid
>     ...
>
> Up to now, it has been only forward references that were vulnerable to
> that sort of thing. Of course running a type checker should pick those
> errors up, but the evaluation of annotations ensures that they are
> actually valid (not necessarily correct, but at least a valid name),
> even if you happen to not be running a type checker. That's useful.
>
> Are we happy to live with that change?
>

Within functions misspellings won't be caught until you invoke a function:

def spam(s):
    return itn(s)  # no error unless spam() is called

We've lived with this for a long time and generally people seem to be happy
with it. The vast majority of code in non-trivial programs (where type
annotations are useful) tends to be within functions, so this will only
slightly increase the number of things that won't be caught without running
tests (or running the type checker).

As type checking has become the main use case for annotations, using
annotations without a type checker is fast becoming a marginal use case.
Type checkers can easily and reliably validate that names in annotations aren't
misspelled.

> * forward references: when a type hint contains names that have not been
> >   defined yet, that definition needs to be expressed as a string
> >   literal;
>
> After all the discussion, I still don't see why this is an issue.
> Strings makes perfectly fine forward references. What is the problem
> that needs solving? Is this about people not wanting to type the leading
> and trailing ' around forward references?
>

Let's make a thought experiment. What if every forward reference would
require special quoting? Would Python programmers be happy with this? Say,
let's use ! as a suffix to mark a forward reference. They make perfectly
fine forward references. They are visually pretty unobtrusive (I'm not
suggesting $ or other ugly perlisms):

def main():
    args = parse_args!()  # A forward reference
    do_stuff!(args)  # Explicit is better than implicit

def parse_args():
    ...

def do_stuff(args):
    ...

Of course, I'm not seriously proposing this, but this highlights the fact
that in normal code forward references "just work" (at least usually), and
if we'd require a special quoting mechanism to use them anywhere, Python
would look uglier and more inconsistent. Nobody would be happy with this
change, even though you'd only have to type a single ! character extra --
that's not a lot work, right?

I think that the analogy is reasonable. In type checked code annotations
are one of most widely used language features -- it's quite possible to
have annotations for almost every function in a code base. This is not a
marginal feature, and people expect commonly used features to feel polished
and usable, not inconsistent and hacky. It's quite possible that the first
type annotated experiment a user writes requires the use of forward
references, and this gives a pretty bad first impression -- not unlike how
the ! forward reference would make the first impression of using
non-type-checked Python pretty awkward.

Here are more arguments why literal escapes are a usability problem:

1) It's hard to predict when string quotes are needed. Real-world large
code bases tend to have a lot of import cycles, and string literal escapes
are often needed within import cycles. However, they aren't always needed.
To annotate code correctly, you frequently need to understand how the file
you are editing is related to other modules in terms of import cycle
structure. In large code bases this can be very difficult to keep in your
head, so basically adding forward references becomes a matter of
tweak-until-it-works. So either each time you write an annotation, you can
look at how imports are structured -- to see whether a particular type
needs to be quoted -- or you can guess and hope for the best. This is a
terrible user experience and increases cognitive load significantly. Our
goal should not be to just have something that technically 'works', as this
is a very low standard. I want Python to be easy to use, intuitive and
elegant. I don't expect that anybody who has annotated large code bases
could consider string literal forward references to be any of those.

2) It's one of the top complaints from users. Even a non-user with a basic
understanding of mypy told me what amounts to "Python doesn't have real
static typing; forward references make it obvious that types are just an
afterthought".

3) It's not intuitive for many programmers, as string literals aren't used
frequently in Python for quoting other kinds of forward references. It may
be intuitive for programmers with a deep Python understanding, but they are
a minority. Other mainstream languages with type annotations don't use
quoting for forward references. C requires forward references to be
declared (but only once per type, not on every use), but I don't think that
C is a good model for Python anyway.

4) If you move chunks of code around, suddenly you may have to update your
forward references -- some quotes won't be needed any more, and new ones
may be required.

5) Some editors and other tools highlight string literals with a color
different from other type annotations, making them look ugly and out of
place. Some might fix this eventually, but you can argue that the current
behavior is reasonable, since forward references are just ordinary string
literals at runtime.

> This PEP is meant to solve the problem of forward references in type
> > annotations.  There are still cases outside of annotations where
> > forward references will require usage of string literals.  Those are
> > listed in a later section of this document.
>
> So the primary problem this PEP is designed to solve, isn't actually
> solved by this PEP.
>

Forward references in type annotations account for the vast majority of
forward references. Everything else is pretty marginal and typically
involves more advanced type system features. We can probably come up with
some data on this. Also, the other contexts are more clearly just regular
Python expressions, so the requirement to use forward references is
arguably less surprising. Consider this example:

FooList = List[Foo]  # Foo only gets defined below

class Foo:
    ...

I think that even a pretty rudimentary understanding of how Python works
should be enough to understand that the above won't work at runtime without
quoting. However, this is less obvious:

class A:
    ...
    def copy(self) -> 'A':  # But wasn't A already declared above?
        return A(...)  # And here we can refer to A directly

Also, forward references already only work sometimes in non-type-checked
Python, so the proposed change would still be kind of consistent with how
things work now. Example:

def f():
    return A()  # Probably ok?

a = A()  # Not ok

f()  # Not ok

class A: ...

f()  # Ok

Static checkers don't see __annotations__ at all, since that's not
> available at edit/compile time. Static checkers see only the source
> code. The checker (and the human reader!) will no longer have the useful
> clue that something is a forward reference:
>

A static checker really doesn't benefit from knowing that something is a
forward reference. Mypy, for example, doesn't need to know if something is
a forward reference to decide what it refers to. As I discussed above,
knowing whether something is a forward reference can actually be less than
helpful for a human reader as well.

Jukka
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-dev/attachments/20171102/4ec76701/attachment-0001.html>


More information about the Python-Dev mailing list