Typeclass

Caching

API

Here are the technical docs about typeclass and how to use it.

Typeclasses for Python.

Basic usage

The first and the simplest example of a typeclass is just its definition:

>>> from classes import typeclass

>>> @typeclass
... def example(instance) -> str:
...     '''Example typeclass.'''

>>> example(1)
Traceback (most recent call last):
...
NotImplementedError: Missing matched typeclass instance for type: int

In this example we work with the default implementation of a typeclass. It raises a NotImplementedError when no instances match. And we don’t yet have a special case for int, that why we fallback to the default implementation.

It works almost like a regular function right now. Let’s do the next step and introduce the int instance for our typeclass:

>>> @example.instance(int)
... def _example_int(instance: int) -> str:
...     return 'int case'

>>> assert example(1) == 'int case'

Now we have a specific instance for int which does something different from the default implementation.

What will happen if we pass something new, like str?

>>> example('a')
Traceback (most recent call last):
...
NotImplementedError: Missing matched typeclass instance for type: str

Because again, we don’t yet have an instance of this typeclass for str type. Let’s fix that.

>>> @example.instance(str)
... def _example_str(instance: str) -> str:
...     return instance

>>> assert example('a') == 'a'

Now it works with str as well. But differently. This allows developer to base the implementation on type information.

So, the rule is clear: if we have a typeclass instance for a specific type, then it will be called, otherwise the default implementation will be called instead.

Protocols

We also support protocols. It has the same limitation as Generic types. It is also dispatched after all regular instances are checked.

To work with protocols, one needs to pass protocol named argument to instance:

>>> from typing import Sequence

>>> @example.instance(protocol=Sequence)
... def _sequence_example(instance: Sequence) -> str:
...     return ','.join(str(item) for item in instance)

>>> assert example([1, 2, 3]) == '1,2,3'

But, str will still have higher priority over Sequence:

>>> assert example('abc') == 'abc'

We also support user-defined protocols:

>>> from typing_extensions import Protocol

>>> class CustomProtocol(Protocol):
...     field: str

>>> @example.instance(protocol=CustomProtocol)
... def _custom_protocol_example(instance: CustomProtocol) -> str:
...     return instance.field

Now, let’s build a class that match this protocol and test it:

>>> class WithField(object):
...    field: str = 'with field'

>>> assert example(WithField()) == 'with field'

See our official docs to learn more!

typeclass(definition: Type[classes._typeclass._AssociatedType]) _TypeClassDef[_AssociatedType][source]
typeclass(signature: classes._typeclass._SignatureType) _TypeClass[_InstanceType, _SignatureType, _AssociatedType, _Fullname]

General case function to create typeclasses.

class AssociatedType(*args, **kwds)[source]

Bases: Generic[classes._typeclass._InstanceType]

Base class for all associated types.

How to use? Just import and subclass it:

>>> from classes import AssociatedType, typeclass

>>> class Example(AssociatedType):
...     ...

>>> @typeclass(Example)
... def example(instance) -> str:
...     ...

It is special, since it can be used as variadic generic type (generic with any amount of type variables), thanks to our mypy plugin:

>>> from typing import TypeVar

>>> A = TypeVar('A')
>>> B = TypeVar('B')
>>> C = TypeVar('C')

>>> class WithOne(AssociatedType[A]):
...    ...

>>> class WithTwo(AssociatedType[A, B]):
...    ...

>>> class WithThree(AssociatedType[A, B, C]):
...    ...

At the moment of writing, https://www.python.org/dev/peps/pep-0646/ is not accepted and is not supported by mypy.

Right now it does nothing in runtime, but this can change in the future.

class Supports(*args, **kwds)[source]

Bases: Generic[classes._typeclass._AssociatedTypeDef]

Used to specify that some value is a part of a typeclass.

For example:

>>> from classes import typeclass, Supports

>>> class ToJson(object):
...     ...

>>> @typeclass(ToJson)
... def to_json(instance) -> str:
...     ...

>>> @to_json.instance(int)
... def _to_json_int(instance: int) -> str:
...     return str(instance)

>>> def convert_to_json(instance: Supports[ToJson]) -> str:
...     return to_json(instance)

>>> assert convert_to_json(1) == '1'
>>> convert_to_json(None)
Traceback (most recent call last):
  ...
NotImplementedError: Missing matched typeclass instance for type: NoneType

You can also annotate values as Supports if you need to:

>>> my_int: Supports[ToJson] = 1

But, this will fail in mypy:

my_str: Supports[ToJson] = 'abc'
# Incompatible types in assignment
# (expression has type "str", variable has type "Supports[ToJson]")

Warning

Supports only works with typeclasses defined with associated types.