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).
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.
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.
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: NamedTuple
s 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.
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.
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
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,
)
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.
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.
It has suggestion.
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).
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.
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.
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: NamedTuple
s 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.
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.
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
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,
)
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.
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.
It has suggestion.