Confusing Type Behavior of typing.NewType construct for Pyright

Playing around with the NewType construct in this Pyright playground gave very confusing results for which I could not use the documentation of the typing module to decipher what was going on.

I defined a NewType called PositiveInt which is also a subtype of int as such.

PositiveInt = NewType("PositiveInt", int)

Confusion Point 1: According to Pyright, variables that are annotated with ANY callable type can be assigned to the value PositiveInt, even when the signature of the annotated type’s dunder call method doesn’t match the actual signature of PositiveInt.

bogus: Callable[[str], str] = PositiveInt
confusion: Callable[[None, None, str], None] = PositiveInt

class What(Protocol):
    def __call__(self, foo: str, bar: bytes) -> str:
        ...

z: What = PositiveInt

Interestingly enough, for each above alias variable, trying to call them with the call signature of their annotated type results in the same errors as trying to directly call PositiveInt with that very call signature.

Confusion Point 2: According to Pyright, a variable annotated with type[PositiveInt] cannot be assigned to the value PositiveInt itself, but a variable annotated with type[type[PositiveInt]] can be assigned to type(PositiveInt).

unconstructable: type[PositiveInt] = PositiveInt 
nested: type[type[PositiveInt]] = type(PositiveInt)

The very fact one can nest type[…] is a bit boggling for my mind.

Confusion Point 3: Pyright can accept a function for which it infers to have a return type “containing“ type[PositiveInt], but it cannot accept a function whose return type is explicitly annotated to “contain“ type[PositiveInt]

def infered_ok(x: int):
    return PositiveInt, PositiveInt(x)

def annotated_bad(x: int) -> tuple[type[PositiveInt], PositiveInt]:
    return PositiveInt, PositiveInt(x)

If all of these behaviors are intentional somehow, is there a reason that indicates why they were implemented? All of these seem rather unintuitive and prone to be used as footguns.

This link doesn’t work properly for me, I think you may have truncated the code= portion of it.

But also just looking at it, you don’t have strict mode enabled, try toggling that and see if any new errors show up?

That’s strange because link address does include the code portion. Just in case, I will post the raw link address as well: Pyright Playground

Here’s another version of that playground with strict mode enabled and with imported references that weren’t used removed. The errors are the same.

Upon further research, the earliest Pyright version (for 3.14) with these unintuitive behaviors seems to be version 1.1.402 Pyright Playground . The strange thing is that Pyright version 1.1.401’s errors Pyright Playground are the ones which are more intuitive and what I had initially expected.

Edit: I was able to replicate the unintuitive callable assignment behavior with any FunctionType Pyright Playground . It seems connected to Pyright incorrectly refers to `NewType` as an instance of `FunctionType` in diagnostic messages, does not allow attributes to be accessed · Issue #10826 · microsoft/pyright · GitHub. I was unable to find existing issues which documented the unintuitive type[PositiveInt] behaviors.

Weird… NewType has the bug for me as well, but the new style syntax for TypeAliasType doesn’t:

from collections.abc import Callable
from typing import NewType

PosInt = NewType('PosInt', int)
type PosInt2 = int
PosInt3 = int

# No Error
x: Callable[[None, None, str], None] = PosInt

# Type Error: TypeAliasType ... not assignable to (None) -> None
y: Callable[[None, None, str], None] = PosInt2

# Type Error: PosInt3 ... not assignable to (None) -> None
z: Callable[[None, None, str], None] = PosInt3

Seems like the NewType is losing the signature of the wrapped type while TypeAliasType and direct aliasing with = properly handles it.

The type alias is of course not equivalent, but I figured checking that those also get caught would be good for clarity.