Does Python Need Type Checking

oleksii.moskalenko@healthjoy.com

Reveng of the types

In the past the world was very clearly divided between interpreted languages with dynamic typing and ahead of time compiled languages with static typing. This is changing as new trends emerge.

I believe we're moving in a future with powerful type systems.    I do not believe that this will be the end of dynamic typing but there appears to be a noticable trend of embracing powerful static typing with local type inference.

 

Armin Ronacher, author of werkzeug

Not All [Static] Type Systems are bad

If the only type systems I knew were C, C++, Java or Go I’d be Ruby Programmer.

 

Paul Snively, not a Ruby Programmer

Languages with good type system:

Rust, OCaml, Haskell?, Agda

 

Benefits of Type Checking

1. Static Analysis

def upgrade_customer(user):
    assert isinstance(user, Customer)

    user.hinted_method_name()

Benefits of Type Checking

2. Correctness

A type system is the most cost effective unit test you’ll ever have.

 

Peter Hallam, C# designer

Benefits of Type Checking

3. Documentation

from typing import NamedTuple, Tuple, Dict, List

Name = str
Duties = List[str]
Address = NamedTuple('Address', [('city', str),
                                 ('country', str),
                                 ('zip_code', str)])
Employee = Tuple[Name, Duties, Address]
EmployeeId = int

Employees = Dict[EmployeeId, Employee]

Competitor Review

Erlang: TypEr & Dialyzer 

-record(mam_msg,
	{key                    :: mam_msg_key(),
	 time                   :: erlang:timestamp(),
	 route                  :: route(),
	 from                   :: ljid(),
	 to                     :: ljid(),
	 stanza                 :: xmlel()}).

...

-spec receive_stanza(jid(), jid(), jid(), xmlel()) -> ok.

receive_stanza(#jid{luser = U, lserver = S} = JID, From, To,
	       #xmlel{name = <<"message">>} = Stanza) ->
    ...

Competitor Review

Clojure: core.typed

(ann summarise
 (IFn [(U nil (NonEmptyColl Int)) -> Int]
      [(U nil (NonEmptyColl Int)) Int -> Int]))


(defn summarise
  ([nseq] (summarise nseq 0))
  ([nseq acc] (if nseq
    (* (summarise (rest nseq)
      (inc acc))
      (first nseq))
    42)))

PEP 484

x = []   # type: List[Employee]
x, y, z = [], [], []  # type: List[int], List[int], List[str]
x, y, z = [], [], []  # type: (List[int], List[int], List[str])

x = [1, 2]  # type: List[int]

with frobnicate() as foo:  # type: int
    # Here foo is an int
    ...

for x, y in points:  # type: float, float
    # Here x and y are floats
    ...

import http.client
errors = {
    'not_found': http.client.NOT_FOUND  # type: ignore
}

PEP 484

from typing import TypeVar, Iterable, Tuple, Callable

T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector) -> T:
    return sum(x*y for x, y in v)


...

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body


...


def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

Implementation: MyPy

def parse_statement(self) -> Tuple[Node, bool]:
        stmt = Undefined  # type: Node
        t = self.current()
        ts = self.current_str()
        is_simple = True  # Is this a non-block statement?
        if ts == 'if':
            stmt = self.parse_if_stmt()
            is_simple = False
        elif ts == 'def':
            stmt = self.parse_function()
            is_simple = False
        elif ts == 'while':
            stmt = self.parse_while_stmt()
            is_simple = False
        elif ts == 'return':
            stmt = self.parse_return_stmt()
        elif ts == 'for':
            stmt = self.parse_for_stmt()
            is_simple = False
        elif ts == 'try':
            stmt = self.parse_try_stmt()
            is_simple = False
        elif ts == 'break':
            stmt = self.parse_break_stmt()
        elif ts == 'continue':
            stmt = self.parse_continue_stmt()
        elif ts == 'pass':
            stmt = self.parse_pass_stmt()
        elif ts == 'raise':
            stmt = self.parse_raise_stmt()
        elif ts == 'import':
            stmt = self.parse_import()
        elif ts == 'from':
            stmt = self.parse_import_from()
        elif ts == 'class':
            stmt = self.parse_class_def()
            is_simple = False
        elif ts == 'global':
            stmt = self.parse_global_decl()
        elif ts == 'nonlocal' and self.pyversion >= 3:
            stmt = self.parse_nonlocal_decl()
        elif ts == 'assert':
            stmt = self.parse_assert_stmt()
        elif ts == 'yield':
            stmt = self.parse_yield_stmt()
        elif ts == 'del':
            stmt = self.parse_del_stmt()
        elif ts == 'with':
            stmt = self.parse_with_stmt()
            is_simple = False
        elif ts == '@':
            stmt = self.parse_decorated_function_or_class()
            is_simple = False
        elif ts == 'print' and (self.pyversion == 2 and
                                'print_function' not in self.future_options):
            stmt = self.parse_print_stmt()
        else:
            stmt = self.parse_expression_or_assignment()

MyPy: Cases

import asyncio
from asyncio import Future

@asyncio.coroutine
def compute(x: int, y: int) -> 'Future[int]':
    print("Compute %s + %s ..." % (x, y))
    yield from asyncio.sleep(0.1)
    return x + y   # Here the int is wrapped in Future[int]

@asyncio.coroutine
def print_sum(x: int, y: int) -> 'Future[None]':
    result = 0 # type: int
    result = yield from compute(x, y)
    

MyPy: Cases

from typing import Undefined
a, b, c = Undefined, Undefined, Undefined # type: (A, B, C)
c = a + c  # Fail
a = a + b  # Fail
c = b + a  # Fail
c = a + b

class A:
    def __add__(self, x: 'B') -> 'C':
        pass
class B:
    pass
class C:
    pass

MyPy: Cases

from typing import TypeVar, Generic, Undefined, List
T = TypeVar('T')

class Container(Generic[T]):
    def __init__(self) -> None:
        pass


class A:
    pass

class B(A):
    pass

ca = Undefined # type: List[Container[A]]
cb = Undefined # type: List[Container[B]]

ab = Container() # type: Container[B]
ba = Container() # type: Container[A]

ca.append(ab)
cb.append(ab) # Fail
cb.append(ba) # Fail

MyPy: The Irony

  • 2195 Test cases
  • 3 Main Classes - 5k LOC
  • Smells like Java

... I also think that this particular tool may be able to help Dropbox convert our own Python 2-based codebase to Python 3.

 

Guido Van R., Dropbox engineer

Possible Flaws

  1. Python 3
  2. C vs Python types
  3. No more ASAP driven development

Thank goodness we don't have only serious problems, but ridiculous ones as well.

Edsger W. Dijkstra

 

Links

  1. http://lucumr.pocoo.org/2014/8/24/revenge-of-the-types/ - Armin Ronacher
  2. https://www.youtube.com/watch?v=SWTWkYbcWU0 - Type Systems - The Good, Bad and Ugly
  3. https://www.python.org/dev/peps/pep-0484/

Does Python Nees Type Checking

By Alexey Moskalenko

Does Python Nees Type Checking

  • 2,355