shoyer
shoyer

Reputation: 9593

Python type annotation for custom duck type

Python's typing module defines a number of duck types, e.g., typing.SupportsAbs to represent any type that implements the __abs__ special method.

Is it possible to define custom duck types in a way such that I can use them as valid type annotations?

For example, I would like to be able to annotate that an argument should be a duck-type equivalent of a threading.Lock, i.e., any object that implements acquire and release methods. Ideally, I could annotate such an argument as SupportsAcquireAndRequire or DuckLock, rather than object.

Upvotes: 10

Views: 2954

Answers (2)

modesitt
modesitt

Reputation: 7210

typing has updated to support use-cases like this one - you may use typing.Protocol for this.

from typing import Protocol, runtime_checkable

@runtime_checkable
class LockLike(Protocol):
    def acquire(self) -> None:
        ...

    def release(self) -> None:
        ...

You need @runtimecheckable if you want to be able to use isinstance as in Martijn's answer.

>>> from threading import Lock
>>> isinstance(Lock(), RuntimeCheckable)
True

Upvotes: 8

Martijn Pieters
Martijn Pieters

Reputation: 1121644

You can define an abstract base class (ABC) to specify the interface:

from abc import ABCMeta, abstractmethod

class SupportsAcquireAndRequire(metaclass=ABCMeta):
    @abstractmethod
    def acquire(self):
        pass

    @abstractmethod
    def release(self):
        pass

    @classmethod
    def __subclasshook__(cls, C):
        for method in ('release', 'acquire'):
            for B in C.__mro__:
                if method in B.__dict__:
                    if B.__dict__[method] is None:
                        return NotImplemented
                    break
            else:
                return NotImplemented
        return True

This is basically how the protocols (like typing.SupportsAbs) are implemented, albeit without directly using ABCMeta.

By giving the ABC a __subclasshook__ method, you can use it in isinstance() and issubclass() tests, which is more than good enough for tools like mypy:

>>> from threading import Lock
>>> isinstance(Lock(), SupportsAcquireAndRequire)
True

Upvotes: 11

Related Questions