Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to hint that a class is implementing a Protocol?

On a path of improvement for my Python dev work. I have interest in testing interfaces defined with Protocol at CI/deb building time, so that if a interface isn't actually implemented by a class we will know immediately after the unit tests run.

My approach was typing with Protocol and using implements runtime_checkable to build unit test. That works, but the team got into a little debate about how to indicate a concretion was implementing a Protocol without busting runtime_checkable. In C++/Java you need inheritance to indicate implementations of interfaces, but with Python you don't necessarily need inheritance. The conversation centered on whether we should be inheriting from a Protocol interface class.

Consider this code example at the end which provides most of the gist of the question. We were thinking about Shape and indicating how to hint to a future developer that Shape is providing IShape, but doing so with inheritance makes the runtime_checkable version of isinstance unusable for its purpose in unit-testing.

There is a couple of paths to improvement here:

We could find a better way to hint that Shape implements IShape which doesn't involve direct inheritance. We could find a better way to check if an interface is implemented at test deb package build time. Maybe runtime_checkable is the wrong idea.

Anyone got guidance on how to use Python better? Thanks!


from typing import (
    Protocol,
    runtime_checkable
)
import dataclasses

@runtime_checkable
class IShape(Protocol):
    x: float


@dataclasses.dataclass
class Shape(IShape):
    foo:float = 0.

s  = Shape()
# evaluates as True but doesnt provide the interface. Undermines the point of the run-time checkable in unit testing
assert isinstance(s, IShape)
print(s.x)  # Error.  Interface wasnt implemented




#
# Contrast with this assert
#
@dataclasses.dataclass
class Goo():
    x:float = 1

@dataclasses.dataclass
class Hoo():
    foo: float = 1

g = Goo()
h = Hoo()
assert isinstance(g, IShape)  # asserts as true
# but because it has the interface and not because we inherited.
print(g.x)


assert isinstance(h, IShape)  # asserts as False which is what we want

like image 793
jrounds Avatar asked Dec 31 '25 23:12

jrounds


1 Answers

When talking about static type checking, it helps to understand the notion of a subtype as distinct from a subclass. (In Python, type and class are synonymous; not so in the type system implemented by tools like mypy.)

A type T is a nominal subtype of type S if we explicitly say it is. Subclassing is a form of nominal subtyping: T is a subtype of S if (but not only if) T is a subclass of S.

A type T is a structural subtype of type S if it something about T itself is compatible with S. Protocols are Python's implementation of structure subtyping. Shape does not not need to be a nominal subtype of IShape (via subclassing) in order to be a structural subtype of IShape (via having an x attribute).

So the point of defining IShape as a Protocol rather than just a superclass of Shape is to support structural subtyping and avoid the need for nominal subtyping (and all the problems that inheritance can introduce).

class IShape(Protocol):
    x: float


# A structural subtype of IShape
# Not a nominal subtype of IShape
class Shape:
    def __init__(self):
        self.x = 3

# Not a structural subtype of IShape
class Unshapely:
    def __init__(self):
        pass


def foo(v: IShape):
    pass

foo(Shape())  # OK
foo(Unshapely())  # Not OK

So is structural subtyping a replacement for nominal subtyping? Not at all. Inheritance has its uses, but when it's your only method of subtyping, it gets used inappropriately. Once you have a distinction between structural and nominal subtyping in your type system, you can use the one that is appropriate to your actual needs.

like image 53
chepner Avatar answered Jan 02 '26 13:01

chepner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!