Type hinting

14 Aug 2014, 2:21 a.m.

Existing implementations for Python

mypy

mypy [1] is a Python variant which checks type annotations statically and at runtime. It supports function annotations, as well as local variable annotations, class attribute type inference, function overloading, type casting, generics, and union types.

None is a valid value for every type.

Examples:

import typing

class BankAccount:
    def __init__(self, initial_balance: int = 0) -> None:
        self.balance = initial_balance
    def deposit(self, amount: int) -> None:
        self.balance += amount
    def withdraw(self, amount: int) -> None:
        self.balance -= amount
    def overdrawn(self) -> bool:
        return self.balance < 0

my_account = BankAccount(15)
my_account.withdraw(5)
print(my_account.balance)

obiwan

obiwan [2] is a library enabling runtime type checking inspired by TypeScript [7] (see Existing Approaches in Other Languages). The syntax leverages function annotations, extending it to validate callback functions, elements of dictionaries and lists. Type checkers might be functions, in which case a type is considered valid if the type checker returns True.

Examples:

def divide(a: int, b: float) -> number:
    return a/b

def robodial(person: {"name":str, "phone": {"type":str, "number":str}}):
    ...

def on_success(callback: function(int, any, ...)):
    ...

pytypedecl

pytypedecl [4] consists of a type declaration language for Python and an optional runtime type checker. Type declarations for module.py are kept in a separate file called module.pytd. This solves issues with declaration ordering.

While initially inspired by the PEP 3107 syntax, pytypedecl diverged to support the following: overloading (specifying the same function multiple times with different argument types), union types (listing multiple possible types for a single argument), generics for collections, and exceptions raised (for documentation purposes).

Example:

class Logger:
  def log(messages: list<str>, buffer: Readable or Writeable) raises IOError
  def log(messages: list<str>) -> None
  def setStatus(status: int or str)

Argument Clinic

Argument Clinic [5] is a preprocessor for CPython C files, automating maintenance of argument parsing code for “builtins”.

Example argument declaration:

/*[clinic input]
os.chmod

    path: path_t(allow_fd='PATH_HAVE_FCHMOD')
        Path to be modified.  May always be specified as a str or bytes.

    mode: int
        Operating-system mode bitfield.

    *

    dir_fd : dir_fd(requires='fchmodat') = None
        If not None, it should be a file descriptor open to a dir, and
        path should be relative; path will then be relative to that
        dir.

    follow_symlinks: bool = True
        If False, and the last element of the path is a symlink, chmod
        will modify the symlink itself instead of the file the link
        points to.

Change the access permissions of a file.
[clinic start generated code]*/

NumPy

NumPy [6] is an extension to Python adding support for multi-dimensional arrays, matrices and operations to operate on those.

The project requires typing information in the API documentation. There is an unambiguous syntax for that type of documentation. Example documentation with types:

ndarray.item(*args)

Copy an element of an array to a standard Python scalar and return it.

Parameters
----------
\\*args : Arguments (variable number and type)

  * none: in this case, the method only works for arrays
    with one element (`a.size == 1`), which element is
    copied into a standard Python scalar object and returned.

  * int_type: this argument is interpreted as a flat index into
    the array, specifying which element to copy and return.

  * tuple of int_types: functions as does a single int_type argument,
    except that the argument is interpreted as an nd-index into the
    array.

Returns
-------
z : Standard Python scalar object
    A copy of the specified element of the array as a suitable
    Python scalar

Existing Approaches in Other Languages

ActionScript

ActionScript [10] is a class-based, single inheritance, object-oriented superset of ECMAScript. It supports inferfaces and strong runtime-checked static typing. Compilation supports a “strict dialect” where type mismatches are reported at compile-time.

Example code with types:

package {
  import flash.events.Event;

  public class BounceEvent extends Event {
    public static const BOUNCE:String = "bounce";
    private var _side:String = "none";

    public function get side():String {
      return _side;
    }

    public function BounceEvent(type:String, side:String){
      super(type, true);
      _side = side;
    }

    public override function clone():Event {
      return new BounceEvent(type, _side);
    }
  }
}

Dart

Dart [9] is a class-based, single inheritance, object-oriented language with C-style syntax. It supports interfaces, abstract classes, reified generics, and optional typing.

Types are inferred when possible. The runtime differentiates between two modes of execution: checked mode aimed for development (catching type errors at runtime) and production mode recommended for speed execution (ignoring types and asserts).

Example code with types:

class Point {
    final num x, y;

    Point(this.x, this.y);

    num distanceTo(Point other) {
        var dx = x - other.x;
        var dy = y - other.y;
        return math.sqrt(dx * dx + dy * dy);
    }
}

Hack

Hack [8] is a programming language that interoperates seamlessly with PHP. It provides opt-in static type checking, type aliasing, generics, nullable types, and lambdas.

Example code with types:

<?hh
class MyClass {
  private ?string $x = null;

  public function alpha(): int {
    return 1;
  }

  public function beta(): string {
    return 'hi test';
  }
}

function f(MyClass $my_inst): string {
  // Will generate a hh_client error
  return $my_inst->alpha();
}

TypeScript

TypeScript [7] is a typed superset of JavaScript that adds interfaces, classes, mixins and modules to the language.

Type checks are duck typed. Multiple valid function signatures are specified by supplying overloaded function declarations. Functions and classes can use generics as type parametrization. Interfaces can have optional fields. Interfaces can specify array and dictionary types. Classes can have constructors that implicitly add arguments as fields. Classes can have static fields. Classes can have private fields. Classes can have getters/setters for fields (like property). Types are inferred.

Example code with types:

interface Drivable {
    start(): void;
    drive(distance: number): boolean;
    getPosition(): number;
}

class Car implements Drivable {
    private _isRunning: boolean;
    private _distanceFromStart: number;

    constructor() {
        this._isRunning = false;
        this._distanceFromStart = 0;
    }

    public start() {
        this._isRunning = true;
    }

    public drive(distance: number): boolean {
        if (this._isRunning) {
            this._distanceFromStart += distance;
            return true;
        }
        return false;
    }

    public getPosition(): number {
        return this._distanceFromStart;
    }
}

Is type hinting Pythonic?

Type annotations provide important documentation for how a unit of code should be used. Programmers should therefore provide type hints on public APIs, namely argument and return types on functions and methods considered public. However, because types of local and global variables can be often inferred, they are rarely necessary.

The kind of information that type hints hold has always been possible to achieve by means of docstrings. In fact, a number of formalized mini-languages for describing accepted arguments have evolved. Moving this information to the function declaration makes it more visible and easier to access both at runtime and by static analysis. Adding to that the notion that “explicit is better than implicit”, type hints are indeed Pythonic.