What are the practical uses of typing.Protocol?

I’m probably just being slow here, but it would seem to me that they are basically ABCs, but without having to inherit from them to make the type-checker happy?

Not having to inherit from the Protocol is exactly the point.

This is useful for “duck typing” (aka. structural typing) in two situations:

  1. You are working with classes that you do not define, so you don’t have control over what they are subclasses of.
  2. You don’t want to use subclass inheritance; maybe you’re defining a plugin system and you don’t want to constrain plugin developers to inheriting from a special base class.
  3. You don’t want to put unnecessary constraints on (or make assumptions about) the types of inputs.

2 and 3 are mostly a matter of taste and opinion. But 1 is a real use case in Python, since so many code bases are untyped and so many “interfaces” and “specifications” exist only as documentation or a PEP, rather than something that can be type-checked statically.

For example, let’s say you want to write a function that transforms the output from a DBAPI / PEP-249 “cursor” object to a list of dicts. Even in a world where every database library developer uses type annotations, you need a generic way to describe “something that behaves like a cursor” without having to rely on the existence of a common base class.

Here is a very rough example:

from typing import (
    Dict,
    Iterable,
    Optional,
    Protocol,
    Sequence,
    Tuple,
)


DbRow = Sequence[object]


class DbCursor(Protocol):
    """ A PEP-249-compliant database cursor """
    description: Tuple[
        str,
        object,
        object,
        object,
        object,
        object,
        object,
    ]

    def execute(self) -> object:
       ...

    def fetchone(self) -> Optional[DbRow]:
        ...

    def fetchall(self) -> Sequence[DbRow]:
        ...


def iterfetch_dicts(cursor: DbCursor) -> Iterable[Dict[str, object]]:
    """ Iterate lazily over rows, yielding each one as a dict """
    colnames = [coldesc.name for coldesc in cursor.description]
    while (row := cursor.fetchone()) is not None:
        yield dict(zip(colnames, row))

This iterfetch_dicts function should type-check against any PEP-249-compliant cursor from any library.

If you didn’t use the Protocol here, you’d have to ask every single database library developer on the planet to add some common base class to their cursor types. Or, you’d have to ask the Steering Council to add a special base class to collections.abc and/or typing, and then you’d have to ask every developer of a static type checker to add support for it.