Run-time behaviour of `TypeAliasType

It already does work:

Python 3.12.0 (tags/v3.12.0:0fb18b0, Oct  2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import TypeAliasType
>>> type x = int
>>> isinstance(x, TypeAliasType)
True

If you mean changing the behavior of this to be based on the aliased type, I’d strongly object. Runtime Type checkers do need more support, but this would be the wrong way about it, for reasons similar in nature to why the TypeForm PEP was recently reinvigorated, and why many type constructs are not compatible with type

It already works with type unions so there is definitely precedent for adding such support. I started looking into makin such a PR back when I created this thread and it would not require many changes. I think it should be done and going forward with a PR is a good way to get a definitive yes or no. And if the answer is no we should consider removing the special casing for unions.

@tmk I’d say go ahead but don’t expect it to be accepted without some resistance :slight_smile:

Reading this discussion again, I think we should not add isinstance() support for type aliases. It’s better to keep the invariants that the type statement creates an alias that is valid in the type system, which is related to but distinct from the class hierarchy that isinstance() uses. Similarly, we should keep the invariant that type aliases are evaluated only when .__value__ is explicitly accessed.

However, we should fix the confusing error text that @ajoino called out in Run-time behaviour of `TypeAliasType - #15 by ajoino. I would accept a PR for that.

3 Likes

I don’t fully understand why we want to differentiate the typing and runtime behaviours this way. And if we do, shouldn’t we go back to change the behaviour of type unions too? Not a rethorical question, I think we should be consistent here.

Edit: forgot to tag @Jelle

It seems like the issue here is that it’s mutable; what if you spelled it as:

A: Final[type] = str

Pyright still complains that this is a variable being used in a type expression but should it?

(Apologies if there’s an obvious “yes” answer to this…)

I touched on this a bit in my earlier messages in the thread. The type statement adds two unique capabilities: lazy evaluation and the use of unambiguously scoped type variables. Both of those capabilities don’t play well with use in isinstance().

I don’t particularly like the fact that isinstance() works with unions, but removing that behavior would be a compatibility break and I don’t see a strong case for doing so.

2 Likes

I maintain a library making heavy use of runtime type introspection and having type aliases be distinct at runtime is actually a very cool thing, so I’d be opposed to such a change.

Being able to attach different behavior at runtime to two “equivalent” types is very powerful. For example, int32 and int16 type aliases to int.

How does any of those two interfere with isinstance checks? In my mind it would just trigger the evaluation so no oroblem with laziness. I also don’t think isinstance cares about type parameters for any other type so would work if it doesn’t “look inside” to check the param. But doing that check doesn’t seem too hard either. I think it would mostly help users and bring the run-time and static type systems closer together.

But as I said earlier, I fine the result of this thread being that the error messages are fixed and the documentation for the new type aliases are very clear that if you use such aliases with isinstance you’re likely doing it wrong.

It doesn’t work well with laziness because it’s unexpected that using isinstance() will result in e.g. a NameError from inside the evaluation of the type alias.

It doesn’t work well with type parameters because runtime isinstance() checks don’t work on parameterized generics. For example, if you write type MyList[T] = list[T], even if we had isinstance() delegate to the __value__ of the alias, it wouldn’t work:

>>> type MyList[T] = list[T]
>>> isinstance([], MyList.__value__)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: isinstance() argument 2 cannot be a parameterized generic

More generally, runtime isinstance() checks can’t see the value of type parameters. We aren’t going to go through the entire list to see whether something is a MyList[int].

1 Like

Ok I think I getcha. Unforturnate.

Alright, you have also convinced me not to pursue this. Though I’m also sad about it.

2 Likes

Given that people are negative about special casing TypeAliasType for isinstance, I assume the same holds true for match-case? The .__value__ looks really clunky :frowning:

# pre PEP695
FuncDef: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
match node:
    case FuncDef(name=name, body=body):
        ...

# post PEP695
type FuncDef = ast.FunctionDef | ast.AsyncFunctionDef
match node:
    case FuncDef.__value__(name=name, body=body):
       ...

(via [match-case] Allow matching Union types · Issue #106246 · python/cpython · GitHub)

Just brain-storming here, what if .__value__ is shortened to something like .T (for type):

type FuncDef = ast.FunctionDef | ast.AsyncFunctionDef
match node:
    case FuncDef.T(name=name, body=body):
       ...

(.t is often used in OCaml to refer to the primary type in a Module, like StringMap.t.)

1 Like

What is a Type Alias Type ? Are we talking about the type of type aliases, or a concrete TypeAlias? These are different, and many would be forgiven to be frustrated if Type Aliasing in python creates Type Alias Types and not Type Aliases (!)

The correctest thing i’ve seen for this is Rust:

pub trait Deref {
    type Target: ?Sized;

    // Required method
    fn deref(&self) -> &Self::Target;
}

Given the purpose of an alias is to serve as a shorter name for some other object,
if TypeAliasType has no Deref[T] for the aliased type,
then it’s not an alias, because it does not serve its intended function, and should be fixed.

truly, type(TypeAlias) == TypeAliasType, but also
type(TypeAlias) == TypeAlias
that’s our quandry

To cut the knot, focus on the intended use case, which is to make type aliases, not types of type aliases, that’s too abstract, that’s like an extensibility issue for libraries, not a core language feature, so the concrete aliases rationally win because they actually work.

given
type X = MyGeneric[Thing]
if we have
type(X) == TypeAliasType
instead of
type(X) == MyGeneric[Thing]
then this is a correctness issue, because the X is a TypeAlias, not a Type Alias Type, we’ve complected (tied together) type(X) and type(type(X)) and that’s why this discussion is muddled.

No need to defend type aliases not aliasing.

Let’s make sure there is an open bug report for TypeAlias not working as intended, as this inappropriately categorizes concrete type aliases as abstract types of type aliases to the point they no longer do their job … as aliases

For me, this is a crucial issue to resolve if the Python type system is going to work in an unsurprising way. Right now, the aliases ain’t aliases, so what’s the point?

class Thing(Protocol):
    @staticmethod
    def yeet(x: Any) -> str:
        return f"{x=} YEETED"


class API(Thing):
    pass


@dataclass(frozen=True, slots=True)
class Stuff[THING: Thing]:
    things: tuple[THING, ...]

    @classmethod
    def new(cls) -> "Stuff[THING]":
        return cls(())

    def from_things(things: Tuple[THING, ...]) -> "Stuff[THING]":
        return Stuff[THING](things)

def test_stuff():
    df1 = Stuff[API].new()
    print(df1)  # > Stuff(things=())
    type TestStuff = Stuff[API]
    df2 = TestStuff.new()
    #               ^^^ AttributeError: 'typing.TypeAliasType' object has no attribute 'new'
    # except, TestStuff is `Deref[Stuff[API]]` which has `.new`
    print(df2)
    assert df1 == df2


test_stuff()

If we NEED multiple types of type aliases, then consider

@runtime_checkable
class Deref[REFERENT](Protocol):
     def deref(self) -> REFERENT: ...
# ... in type.__init__ or wherever
    if isinstance(self, Deref[R]):
        return self.deref()

see also Deref in std::ops - Rust

which leads to the question
how do we add associated types to python

1 Like