PEP 526 - var annotations and the spirit of python

Steven D'Aprano steve+comp.lang.python at pearwood.info
Wed Jul 4 11:31:16 EDT 2018


On Wed, 04 Jul 2018 13:48:26 +0100, Bart wrote:

> Presumably one type hint applies for the whole scope of the variable,
> not just the one assignment. 

You know how in C you can write 

    int x = 1;  # the type applies for just this one assignment
    x = 2.5;    # perfectly legal, right?


Wait, no, of course you can't do that. Why would you suggest that as even 
a possibility?

Of course the type (whether inferred or annotated) applies for the entire 
scope of that variable.


> Which means that here:
> 
>     x: int = 3
>     x = f(x)
> 
> you know x should still an int after these two statements, because the
> type hint says so. Without it:
> 
>     x = 3
>     x = f(x)
> 
> x could be anything.

That's not how type checking works. It makes *no difference* whether the 
type is inferred or hinted. Type hints only exist to cover the cases the 
type inference engine can't determine, or determine too strictly. See 
below.

In the Dark Ages of type-checking, the compiler was too dumb to work out 
for itself what the type of variables is, so you have to explicitly 
declare them all, even the most obvious ones. Given such a declaration:

    int x = 3;  # using C syntax

the type checker is smart enough to look at the next line:

    x = f(x);

and complain with a type-error if f() returns (say) a string, or a list. 
Checking that the types are compatible is the whole point of type 
checking.

Now fast forward to the Enlightenment of type-inference, first used in a 
programming language in 1973 (so older than half the programmers alive 
today). That purpose doesn't go away because we're using type inference.

With type-inference, the type-checker is smart enough to recognise what 
type a variable is supposed to be (at least sometimes):

    x = 3;  # of course it's an int, what else could it be?
    x = f(x);

and likewise complain if f(x) returns something other than an int. 
There's no point in type checking if you don't, you know, actually 
*check* the types.

With type inference, the only reason to declare a variable's type is if 
the type checker can't infer it from the code, or if it infers the wrong 
type. (More on this later.)

To do otherwise is as pointless and annoying as those comments which 
merely repeat what the code does:

    import math       # import the math module
    mylist.append(v)  # append v to mylist
    counter += 1      # add 1 to counter
    s = s.upper()     # convert s to uppercase
    x: int = 3        # assign the int 3 to x

Don't be That Guy who writes comments stating the bleeding obvious.

There's not always enough information for the type checker to infer the 
right type. Sometimes the information simply isn't there:

    x = []  # a list of what?

and sometimes you actually did intend what looks like a type-error to the 
checker:

    x = 3       # okay, x is intended to be an int
    x = "spam"  # wait, this can't be right
    

In the later case, you can annotate the variable with the most general 
"any type at all" type:

    from typing import Any
    x: Any = 3  # x can be anything, but happens to be an int now
    x = "spam"  # oh that's fine then


or you can simply not check that module. (Type checking is optional, not 
mandatory.)



>> A better example would be:
>> 
>>      x: int = None
>> 
>> which ought to be read as "x is an int, or None, and it's currently
>> None".
> 
> In that case the type hint is lying.

"Practicality beats purity."

"This type, or None" is such a common pattern that any half-way decent 
type checker ought to be able to recognise it. You can, of course, 
explicitly annotate it:

    x: Optional[int] = None

but the type checker should infer that if you assign None to a variable 
which is declared int, you must have meant Optional[int] rather than just 
int. If it doesn't, get a better type checker.

Note that None is a special case (because sometimes special cases *are* 
special enough to break the rules). This should be an error:

    x: int = "hello world"  # wait, that's not an int

But this is fine:

    x: Union[int, str] = "hello world"

x is permitted to be an int or a string, and it happens to currently be a 
string.


> If both x and y have type hints of 'int', then with this:
> 
>     z = x + y
> 
> you might infer that z will be also 'int'

Indeed. If you inferred that z was a list, you're doing it wrong :-)


> (whether it it type hinted or not). But if either of x and y can 
> be None, then this might not even execute.

If x or y could be None, the type checker should complain that None does 
not support the + operator. The whole point of type checking is to find 
bugs like that at compile time. A type checker that doesn't, you know, 
*actually find type bugs* is a waste of time and effort.

This is Type-Checking 101 stuff. A type checker which can't even 
recognise that None+1 is a type error is a pretty crappy type checker. 
Why do you assume that the state of the art in type-checkers in 2018 is 
*worse* than the state of the art in 1953 when Fortran came out?



> If something is an int, then make it an int:
> 
>     x: int = 0

Indeed, that's often the best way, except for the redundant type hint, 
which makes you That Guy:

    x: int = 0  # set x to the int 0


But the point of my example was that x is not an int. It is an optional 
int, that is, an int or None.





-- 
Steven D'Aprano
"Ever since I learned about confirmation bias, I've been seeing
it everywhere." -- Jon Ronson



More information about the Python-list mailing list