Python equivalent of Typescript interface

Question:

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).

Asked By: Otto

||

Answers:

With Python 3.5, you can use annotations to specify the type of parameters and return types. Most of recent IDE, like PyCharm can interpret those annotations and give you good code completion. You can also use a comment to specify the signature of a function, or the type of a variable.

Here is an example:

from typing import List, Optional


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


class Person(object):
    def __init__(self, name: str, addresses: List[Address]):
        self.name = name
        self.addresses = addresses


person = Person(
    name='Joe',
    addresses=[
        Address(street='Sesame', housenumber=1),
        Address(street='Baker', housenumber=221, housenumber_postfix='b')
    ])

Notice that Python is not a strongly typed language. So, annotations are only a guide for developers. If you really want to check your code, you need an external tools (currently, the best one is mypy). It can be used like any other code checker during code quality control.

Answered By: Laurent LAPORTE

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.

Answered By: hoefling

Python 3.6 added a new implementation of namedtuple that works with type hints, which removes some of the boilerplate required by the other answers.

from typing import NamedTuple, Optional, List


class Address(NamedTuple):
    street: str
    housenumber: int
    housenumberPostfix: Optional[str] = None


class Person(NamedTuple):
    name: str
    adresses: List[Address]


person = Person(
    name='Joe',
    adresses=[
        Address(street='Sesame', housenumber=1),
        Address(street='Baker', housenumber=221, housenumberPostfix='b'),
    ],
)

Edit: NamedTuples are immutable, so be aware that you can’t use this solution if you want to modify the fields of your objects. Changing the contents of lists and dicts is still fine.

Answered By: Brian Schlenker

A simple solution I found (that doesn’t require Python 3.7) is to use SimpleNamespace:

from types import SimpleNamespace as NS
from typing import Optional, List

class Address(NS):
    street: str
    housenumber: int
    housenumber_postfix: Optional[str]=None


class Person(NS):
    name: str
    addresses: List[Address]


person = Person(
    name='Joe',
    addresses=[
        Address(street='Sesame', housenumber=1),
        Address(street='Baker', housenumber=221, housenumber_postfix='b')
    ])
  • This works in Python 3.3 and higher
  • The fields are mutable (unlike NamedTuple solution)
  • Code completion seems to work flawlessly in PyCharm but not 100% in VSCode (raised an issue for that)
  • Type checking in mypy works, but PyCharm does not complain if I e.g do person.name = 1

If anyone can point out why Python 3.7’s dataclass decorator would be better I would love to hear.

Answered By: Otto

Perhaps this will work well with mypy

from typing import List
from mypy_extensions import TypedDict

EntityAndMeta = TypedDict("EntityAndMeta", {"name": str, "count": int})

my_list: List[EntityAndMeta] = [
  {"name": "Amy", "count": 17},
  {"name": "Bob", "count": 42},
]

Read more about TypedDict from the mypy docs or from the source code

I’m pretty sure you can nest these things, and set some of them to Optional if you’d like.

I got this idea from https://stackoverflow.com/a/21014863/5017391

Answered By: Boris Yakubchik

A TypeScript interface describes a JavaScript object. Such an object is analogous to a Python dictionary with well-known string keys, which is described by a mypy TypedDict.

TypeScript interface example

For example the TypeScript interface:

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

will describe JavaScript objects like:

var someAddress = {
    street: 'SW Gemini Dr.',
    housenumber: 9450,
};

mypy TypedDict example

The equivalent mypy TypedDict:

from typing import TypedDict

class Address(TypedDict):
    street: str
    housenumber: int

will describe Python dictionaries like:

some_address = {
    'street': 'SW Gemini Dr.',
    'housenumber': 9450,
}

# or equivalently:

some_address = dict(
    street='SW Gemini Dr.',
    housenumber=9450,
)

These dictionaries can be serialized to/from JSON trivially and will conform to the analogous TypeScript interface type.

Note: If you are using Python 2 or older versions of Python 3, you may need to use the older function-based syntax for TypedDict:

from mypy_extensions import TypedDict

Address = TypedDict('Address', {
    'street': str,
    'housenumber': int,
})

Alternatives

There are other ways in Python to represent structures with named properties.

Named tuples are cheap and have read-only keys. However they cannot be serialized to/from JSON automatically.

from typing import NamedTuple

class Address(NamedTuple):
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

Data classes, available in Python 3.7, have read-write keys. They also cannot be serialized to/from JSON automatically.

from dataclasses import dataclass

@dataclass
class Address:
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

Simple namespaces, available in Python 3.3, are similar to data classes but are not very well known.

from types import SimpleNamespace

class Address(SimpleNamespace):
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)

attrs is a long-standing third-party library that is similar to data classes but with many more features. attrs is recognized by the mypy typechecker.

import attrs

@attr.s(auto_attribs=True)
class Address:
    street: str
    housenumber: int

my_address = Address(
    street='SW Gemini Dr.',
    housenumber=9450,
)
Answered By: David Foster

Try https://github.com/cs-cordero/py-ts-interfaces

It looks pretty nice. Quote:

In web applications where Python is used in the backend and TypeScript
is used in the frontend, it is often the case that the client will
make calls to the backend to request some data with some specific
pre-defined "shape". On the client-side, an interface for this data is
usually defined and if the Python backend authors use typechecking,
like with mypy, the project authors may be typing the JSON response
values as well.

This results in a duplication of code. If the shape changes in the
backend, the related interface must also be reflect its changes in the
frontend. At best, this is annoying to maintain. At worst, over time
the interfaces may diverge and cause bugs.

This library aims to have a single source of truth that describes the
shape of the payload between the backend and the frontend.

Answered By: laike9m

What do you guys think about pydantic?
python 3.10

from pydantic import BaseModel


class Address(BaseModel):
    street: str
    housenumber: int
    housenumberPostfix: str | None = None


class Person(BaseModel):
    name: str
    adresses: list[Address]


person: Person = Person(
    name="Joe",
    adresses=[
        Address(street="Sesame", housenumber=1),
        Address(street="Baker", housenumber=221, housenumberPostfix="b"),
    ],
)

When we input wrong type.

Editor complain wrong type

It has suggestion.

Editor show suggestion

Answered By: hidayat hamir