Type hinting vs duck typing

Question:

One of the cons of using type hinting in Python is trading beauty of Python code.

Before type hinting my method signatures were concise:

def echo(items):
    for i in items:
        print(i)
    

Since my team is using type hinting, I’ve added type hints to my code as well:

def echo(items: Set[str]) -> None:

Still quite leggible. After some time other parts of my code that operate on set of sets required my items to be hashable, while others did not. So I decided to support frozenset as well and now my method looks like:

def echo(items: Union[Set[str],Frozenset[str]]) -> None:
 

It started to look like methods in Java, although in Java I could operate on interfaces, ignoring implementation details:

void echo(Set<String> items) {

Python does not support interface concept, i.e. I cannot state that Set implements Frozenset. The initial implementation would work both for Set and Frozenset thanks to duck typing: both behave as set. However, my impression is that explicit type hinting somehow does not play well with duck typing

How can I find a good balance between typing hinting and duck typing?

Asked By: dzieciou

||

Answers:

Use AbstractSet:

from typing import AbstractSet

def echo(items: AbstractSet[str]) -> None:
    ...

Both Set and FrozenSet inherit (directly or indirectly) from AbstractSet:

AbstractSet
    |
    |
    +--------------+
    |              |
    |              |
MutableSet        FrozenSet
    |
    |
   Set
Answered By: chepner

As said @chpner it’s a good option to use built-in types. All abstract types from the typing module are protocols starting from Python 3.7.

The conception of Protocols has some similarities with interfaces.

As said in the docs:

Structural subtyping can be seen as a static equivalent of duck typing, which is well known to Python programmers. Mypy provides support for structural subtyping via protocol classes described below. See PEP 544 for the detailed specification of protocols and structural subtyping in Python.

Also is possible to define custom protocols:

from typing import Iterable
from typing_extensions import Protocol

class SupportsClose(Protocol):
    def close(self) -> None:
       ...  # Empty method body (explicit '...')

class Resource:  # No SupportsClose base class!
    # ... some methods ...

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

def close_all(items: Iterable[SupportsClose]) -> None:
    for item in items:
        item.close()

close_all([Resource(), open('some/file')])  # Okay!

While the main purpose of protocols is static analysis they allow to check if the object follows some protocol in runtime as well:

from typing_extensions import Protocol, runtime_checkable

@runtime_checkable
class Portable(Protocol):
    handles: int

class Mug:
    def __init__(self) -> None:
        self.handles = 1

mug = Mug()
if isinstance(mug, Portable):
   use(mug.handles)  # Works statically and at runtime
Answered By: skhalymon
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.