Python equivalent of Typescript interface

Otto picture Otto · Jan 14, 2018 · Viewed 18.6k times · Source

Recently I have been working with Typescript a lot, it allows to express things like:

interface Address {
    street: string;
    housenumber: number;
    housenumberPostfix?: string;
}

interface Person {
    name: string;
    adresses: Address[]
}

const person: Person = {
    name: 'Joe',
    adresses: [
        { street: 'Sesame', housenumber: 1 },
        { street: 'Baker', housenumber: 221, housenumberPostfix: 'b' }
    ]
}

Pretty concise and giving all the luxuries as type checking and code completion while coding with Persons.

How is this done in Python?

I have been looking at Mypy and ABC but did not yet succeed in finding the pythonic way to do something similar as the above (my attempts resulted in way too much boilerplate to my taste).

Answer

hoefling picture hoefling · Jan 15, 2018

For the code completion and type hinting in IDEs, just add static typing for the Person and Address classes and you are already good to go. Assuming you use the latest python3.6, here's a rough equivalent of the typescript classes from your example:

# spam.py
from typing import Optional, Sequence


class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]

    def __init__(self, street: str, housenumber: int, 
                 housenumber_postfix: Optional[str] = None) -> None:
        self.street = street
        self.housenumber = housenumber
        self.housenumber_postfix = housenumber_postfix


class Person:
    name: str
    adresses: Sequence[Address]

    def __init__(self, name: str, adresses: Sequence[str]) -> None:
        self.name = name
        self.adresses = adresses


person = Person('Joe', [
    Address('Sesame', 1), 
    Address('Baker', 221, housenumber_postfix='b')
])  # type: Person

I suppose the boilerplate you mentioned emerges when adding the class constructors. This is indeed inavoidable. I would wish default constructors were generated at runtime when not declared explicitly, like this:

class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]


class Person:
    name: str
    adresses: Sequence[Address]


if __name__ == '__main__':
    alice = Person('Alice', [Address('spam', 1, housenumber_postfix='eggs')])
    bob = Person('Bob', ())  # a tuple is also a sequence

but unfortunately you have to declare them manually.


Edit

As Michael0x2a pointed out in the comment, the need for default constructors is made avoidable in python3.7 which introduced a @dataclass decorator, so one can indeed declare:

@dataclass
class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]


@dataclass
class Person:
    name: str
    adresses: Sequence[Address]

and get the default impl of several methods, reducing the amount of boilerplate code. Check out PEP 557 for more details.


I guess you could see stub files that can be generated from your code, as some kind of interface files:

$ stubgen spam  # stubgen tool is part of mypy package
Created out/spam.pyi

The generated stub file contains the typed signatures of all non-private classes and functions of the module without implementation:

# Stubs for spam (Python 3.6)
#
# NOTE: This dynamically typed stub was automatically generated by stubgen.

from typing import Optional, Sequence

class Address:
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]
    def __init__(self, street: str, housenumber: int, housenumber_postfix: Optional[str]=...) -> None: ...

class Person:
    name: str
    adresses: Sequence[Address]
    def __init__(self, name: str, adresses: Sequence[str]) -> None: ...

person: Person

These stub files are also recognized by IDEs and if your original module is not statically typed, they will use the stub file for type hints and code completion.