Allow using protocol classes as bases of `NewType`

Currently, both Mypy and Pyright forbids using protocol classes as bases of NewType. I don’t see any good reason in this behavior.

For example:

from typing import NewType, Protocol

class Base(Protocol):
    def foo(self, /) -> int: ...

class Derived:
    def foo(self, /) -> int:
        return 0

NewDerived = NewType("NewDerived", Base)  # ? Currently not supported.

derived = Derived()
new_derived = NewDerived(derived)

def f0(x: Base) -> None: pass
f0(derived)
f0(new_derived)

def f1(x: NewDerived) -> None: pass
f1(derived)  # ! Should fail type-checking.
f1(new_derived)

Code above runs without any problem, and passes and fails type-checks as expected in both Mypy and Pyright except for the line that defines NewDerived. I believe that it should be safe for such usages.

The reason this is disallowed is because it makes no sense, it’s equivalent to a regular type alias, because the type’s structure is still the same, so the two types are equivalent, the derived type cannot reject the base type.

That being said you can create a nominal marker subclass with dummy implementations for each of the attributes and use a custom __new__ to mimic the behavior of NewType. I think I posted this recipe already in an earlier discussion about this very case.

And here it is:

it’s equivalent to a regular type alias, because the type’s structure is still the same, so the two types are equivalent, the derived type cannot reject the base type.

Please explain this one. I’m afraid I don’t get it.

In my understanding, when we have

NewDerived = NewType("NewDerived", Base)

it is like saying

class NewDerived(Base):
    def __new__(cls, base: Base) -> Self: ...

When Base is a protocol class, NewDerived is not. Why are the two types equivalent?

Perhaps a better way to put it, is that it’s confusing and non-obvious what should happen. You could just as easily argue that you should get back another structural type, because you started with one. Even if that would be useless, since that would make both the types structurally equivalent.

1 Like

In the eye of a type checker, NewDerived should definitely be a nominal (and concrete) class, otherwise NewDerived(...) would not be allowed when it is the foundation usage of a new type.