A primer on creating custom types in Python
• • ☕️ 4 min readIntro
Since Python 3, classes and types are pretty much the same thing.
A class is a blueprint for creating objects, which defines the attributes and methods that the objects will have. When you create an instance of a class, you are creating an object with those attributes and methods.
A type is the category to which an object belongs. Every object in Python has a type, which
defines the set of operations that can be performed on it. For example, the type of an integer is int
and the type
of a string is str
.
Classes are used to create new types. When you define a class, you are defining a new type of object. The type of the objects created from a class is the class itself.
Consider the following class definition:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
In this case, Person
is a class that defines the structure and behavior of a Person
object.
When this class is defined, it creates a new type object called Person
.
This type object can be used to create new instances of the Person
class
>>> alice = Person("Alice", 25)
>>> type(alice)
<class '__main__.Person'>
This type can be used to statically type your code.
def get_person() -> Person:
return Person(name="Alice", age=25)
1. typing.TypeVar PEP 484
added in Python 3.5
typing.TypeVar
allows you to create type variables that can be used to specify types in a flexible way. Type variables can be used to define generic functions, classes, and protocols that can work with different types.
import random
import typing
T = typing.TypeVar('T')
def get_random_item(items: typing.List[T]) -> T:
return random.choice(items)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
alice = Person("Alice", 25)
bob = Person("Bob", 35)
get_random_item([alice, bob])
get_random_item(['not alice', 'not bob'])
In this example, we create a type variable T
using TypeVar('T')
, which represents an unknown type. We can use this type variable to define a function that takes a list of items of type T
and returns a random item from that list, which is also of type T
.
TypeVar
shines when used with Generic
, if you can find a use for it in your codebase.
import typing
T = typing.TypeVar('T')
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class ObjectsStore(typing.Generic[T]):
def __init__(self):
self.objects: list[T] = []
def append(self, obj: T):
self.objects.append(obj)
class PeopleStore(ObjectsStore[Person]):
pass
people_store = PeopleStore()
alice = Person("Alice", 25)
people_store.append(alice)
people_store.append('asd')
In the dummy example above, when creating a class PeopleStore
, we tell it to inherit from ObjectsStore
and that the type T
is Person
, so all objects within PeopleStore
need to be of type People
.
When we run mypy on this, we see
main.py:29: error: Argument 1 to “append” of “ObjectsStore” has incompatible type “str"; expected "Person” [arg-type]
2. typing.NewType PEP 484
added in Python 3.5
typing.NewType
is a typing module that allows you to create new types that are NOT aliases for existing types.
from typing import NewType
PersonAge = NewType("PersonAge", int)
class Person:
def __init__(self, name, age: PersonAge):
self.name = name
self.age = age
def __str__(self):
return self.name
alice = Person("Alice", 25)
Even though PersonAge
was derived from int
type, mypy
complains.
main.py:15: error: Argument 2 to “Person” has incompatible type “int"; expected "PersonAge” [arg-type]
We’re forced to cast it to the correct type, even though it’s an instance of int
.
>>> alice = Person("Alice", PersonAge(25))
>>> isinstance(PersonAge(25), int)
True
>>> alice.age.__class__.mro()
[<class 'int'>, <class 'object'>]
>>> alice.age is 25
True
I think it’s best to think about it as a special kind of alias, because static type checkers will know not to treat it as such.
3. typing.TypeAlias PEP 613
added in Python 3.10
typing.TypeAlias
allows you to create a new name for an existing type. It is essentially an alias for the type, which means that the new type is equivalent to the original type for both - human and static type checker.
from typing import TypeAlias
PersonAge: TypeAlias = int
class Person:
def __init__(self, name, age: PersonAge):
self.name = name
self.age = age
def __str__(self):
return self.name
alice = Person("Alice", 25)
assert isinstance(alice.age, int)
assert type(alice.age) == int
4. types.GenericAlias PEP 585
added in Python 3.9
I’m not sure what’s the use case for it and I assume I will never find one, as typing using the standard library has improved a lot since GenericAlias was added. For example, these two are identical:
>>> assert GenericAlias(list, Person, ) == list[Person]
>>> True
One use case for GenericAlias is to create custom generic types that are not included in the typing module. For example, suppose you want to create a generic type Pair that represents a pair of two values of potentially different types. You could define it like this:
import types
class Pair:
def __class_getitem__(cls, params):
return types.GenericAlias(cls, params)
PairType = Pair[str, int] # creates a GenericAlias representing Pair[str, int]
In this example, Pair is a custom class that has a __class_getitem__
method, which is called when the class is parameterized with type arguments. This method returns a GenericAlias object that represents the parameterized class.
Though it doesn’t look very useful to me.