80

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

Otto
  • 1,787
  • 1
  • 17
  • 25

8 Answers8

60

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.

hoefling
  • 59,418
  • 12
  • 147
  • 194
  • 2
    Class attributes are useless here. – Laurent LAPORTE Jan 14 '18 at 23:28
  • 1
    @LaurentLAPORTE: no they aren't - remove them and the type hints for class attributes are gone: `Person.name.>` – hoefling Jan 15 '18 at 00:24
  • 5
    Regarding your second code snippet -- you may be interested in [PEP 557](https://www.python.org/dev/peps/pep-0557/), which was recently accepted (just in time for Python 3.7!) and is designed specifically to solve that exact pain point you brought up. – Michael0x2a Jan 15 '18 at 04:02
  • @hoefling: Sorry to insist on it. I reformulate: class attributes are useless, only instance attributes are useful here. So `Person.name.>` is a nonsense, but `person.name.>` is OK. You are overspecifying. – Laurent LAPORTE Jan 15 '18 at 08:25
  • 3
    @LaurentLAPORTE -- interestingly, it seems like a semi-new convention is to actually use class annotations as a way of indicating what instance attributes are required. See PEP 526 (in particular, the [section about class annotations](https://www.python.org/dev/peps/pep-0526/#id9)) for an example. [This question](https://stackoverflow.com/q/47532472/646543) (and the answer I left there) goes into this in a little more detail. One thing to note, however, is that this answer does NOT use any class *attributes*. It uses several class *annotations*, which are not the same thing. – Michael0x2a Jan 15 '18 at 19:08
17

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 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,
};

Python TypedDict example

The equivalent Python 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.

Data classes, available in Python 3.7, have read-write keys. However they 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,
)

Named tuples are cheap and have read-only keys. They also 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,
)

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,
)
David Foster
  • 6,931
  • 4
  • 41
  • 42
7

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.

Brian Schlenker
  • 4,966
  • 6
  • 31
  • 44
  • Great! And usable in py3.6 so i don't have to wait for 3.7's dataclasses. Question do you know why the optional construct is needed as the type checker could infer optionality from the default argument – Otto Jan 24 '18 at 05:53
  • 1
    The typing documentation says "If necessary, Optional[t] is added for function and method annotations if a default value equal to None is set" so maybe it isn't required. I did read somewhere that this functionality would be deprecated in later python versions though. Something to do with this implicit optional breaking things in some edge cases. I'll add link if I can find where I read that. – Brian Schlenker Jan 24 '18 at 08:17
  • 1
    There's a big discussion here: https://github.com/python/typing/issues/275 – Brian Schlenker Jan 24 '18 at 08:20
6

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.

Otto
  • 1,787
  • 1
  • 17
  • 25
5

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

Boris Yakubchik
  • 3,861
  • 3
  • 34
  • 41
2

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.

laike9m
  • 18,344
  • 20
  • 107
  • 140
1

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.

Laurent LAPORTE
  • 21,958
  • 6
  • 58
  • 103
  • Minor corrections: `s/Python 3/Python 3.5/`, also subclassing from `object` is required in `python2` only (but is still valid in `python3`, so not an error). – hoefling Jan 14 '18 at 23:16
  • Ok thanks. In your classes each instance variable is keyed in by you 3 times. Is there a way without that boilerplate? – Otto Jan 15 '18 at 08:17
  • 2
    Python is strongly typed. – ruohola Jun 26 '20 at 19:14
0

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