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