NewType diamond inheritance allowed?

Is there a correct way to NewType a diamond inheritance? The docs don’t seem to address it, and pyright seems to indicate no. At runtime, since NewType is a fast return, python itself is extremely permissive of what goes as the second argument, so it’s hard to tell.

Motivation/MWE:

I have some array routines that lead to bugs because of numerical math, i.e. bugs that are tricky to find and troubleshoot. I was hoped introducing types for the math abstractions could help prevent mistakes and improve annotations. Take the following minimal example of some optimization alternates between updates to the bounds of some solution and the solution itself:

StateVector = NewType("StateVector", np.ndarray)
InequalityConstraint = NewType("InequalityConstraint", np.ndarray)

def new_bounds(current_state: StateVector, last_state: StateVector) -> InequalityConstraint:
    """Calculate next inner optimization bounds"""

def inner_optimization(f: Callable, x0: StateVector, lower_bound: InequalityConstraint) -> StateVector:
    """Optimize something, given x0 >= InequalityConstraint"""

IIUC, the type checker would cause an alarm if someone tried to use a StateVector in place of an InequalityConstraint, or if they used the latter in a math routine that required equality constraints. Moreover, these abstractions feel like they help clarify intent and provide an easy stepping stone to full-blown classes when additional functionality is needed, without prematurely adding features.

However, the problem is when my algorithm allows sparse matrices or some other duck-typing. A type alias to a Union can’t be NewType’d, at least according to Pylance/pyright:

Foo = NewType("Foo", int | str)

Pylance: Expected class as second argument to NewType

But I thought the second argument was a type, not a class, according to PEP 483 :man_shrugging:. Are diamond diagram subtypes possible without an actual subclass?

1 Like

(edit: this is not correct)

This is the problem. When you do this:

UserId = NewType("UserId", int)
uid = UserId(1234)

The UserId(1234) call just returns int(1234). This means that int (the second argument to UserId) must be callable – i.e., a class, not a type. A union like int|str is not callable. If you want to do multiple inheritance, you have to do it explicitly as a class, and then call NewType on the subclass:

class A: pass
class B: pass

class Both(A, B): pass

Foo = NewType("Foo", Both)

This means that int (the second argument to UserId) must be callable

It doesn’t seem so? The typing docs say:

At runtime, the statement Derived = NewType('Derived', Base) will make Derived a callable

So UserId(1234) doesn’t return int(1234), it returns 1234. And indeed, NewType’s implementation creates an object with a __call__ attribute that is the identity function.

The second argument to NewType(name, tp) is just assigned to the NewType object’s __supertype__ attribute, but it’s unclear what it does or why it needs to be callable.

Oop, I guess you’re right. Looks like I always got NewType wrong.