PEP 695 type aliases not suitable as replacement for typing.TypeAlias?

I reported this in github, but I am wondering if my sense of what people use type aliases for is completely wrong.

But it seems to me that although PEP 695 type aliases are the blessed replacement for the now-deprecated typing.TypeAlias, they are functionally not suitable as replacement.

From the issue:

A type alias designated as such using the typing.TypeAlias annotation could be used in many places that a PEP 695 type aliases cannot, including (at least) as a class’s parent class, in calls to isinstance and issubclass . All of of these have been used in various codebases I maintain.

As such typing.TypeAlias should be un-deprecated, or the implementation of PEP 695 type aliases should be updated to work in all cases where a plain type should be.

With typing.TypeAlias :

$ poetry run python3
Python 3.12.0 (main, Oct  4 2023, 06:27:34) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import typing
>>> StringAlias: typing.TypeAlias = str
>>> isinstance('test', StringAlias)
True
>>> issubclass(StringAlias, str)
True
>>> class GreaterString(StringAlias): pass
... 
>>> 

With PEP 695 type aliases:

$ poetry run python3
Python 3.12.0 (main, Oct  4 2023, 06:27:34) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> type StringAlias = str
>>> isinstance('test', StringAlias)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union
>>> issubclass(StringAlias, str)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: issubclass() arg 1 must be a class
>>> class GreaterString(StringAlias): pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: typealias() takes exactly 2 positional arguments (3 given)
>>> 

The examples above are moderately trivial, but my actual use case is to make extremely long class names (typically generic classes) more manageable.

An actual of example of this being:

type ChassisMountable = _equip.Mountable[ChassisMount,ChassisSectionDescription]

class Engine(ChassisMountable, _equip.FixedMass, _equip.Equipment): ...

[snip: Several dozen other similar classes that also extend ChassisMountable]

My issue with using type aliases this way, is that there’s many typing special forms that are not valid runtime types and it’s not actually possible for the type checker to detect that currently, so I don’t think this pattern should really be encouraged, if your intention is to use a type at runtime, it should probably just be regular assignment, I wouldn’t even use TypeAlias in this case.

In your example I would use ChassisMountable: type[_equip.Mountable[ChassisMount,ChassisSectionDescription]] = _equip.Mountable[ChassisMount,ChassisSectionDescription] to indicate that we’re talking about an actual instance of type or omit the type annotation entirely, which should infer ChassisMountable to the same type.

Union happens to work in isinstance/issubclass but it would be an invalid base class and there’s many other special typing forms that don’t work in isinstance/issubclass.

That being said, should type.__call__/__instancecheck__/__subclasscheck__ be extended to be able to resolve TypeAliasType? Maybe, but it feels like an unnecessary complication in the runtime.

The reason it works with TypeAlias is entirely incidental, it almost certainly wasn’t intentional.

1 Like

Yeah I hear you that there’s different ways to do it. I prefer using an actual type alias (of either sort) because it’s both explicit about the intention (as opposed to wanting to define an variable that happens to hold a class) which was the OG PEP’s rationale, and no less importantly it’s also shorter, making it easier to understand and maintain, both of which I value highly.

The fact that TypeAlias is compatible with regular uses of class objects in Python may have been accidental (or not, I’m not sure), but PEP 613 was explicitly aiming to be compatible with the traditional method of defining them (that is, using a regular variable to store the type). And in doing so it also ensured TypeAlias annotated types are also compatible with how the traditional method is used, which includes all of the uses I am looking for above.

Which makes it super puzzling that PEP 695 type aliases are supposed to be replacements for TypeAlias annotations, since they aren’t compatible with many of the ways TypeAlias or traditional variable-based aliases have been used since forever.

I think it’s important to distinguish what is a valid type and what is a valid type annotation. All valid types are valid type annotations, but not all valid type annotations are valid types.

TypeAlias only ensures a valid type annotation, not a valid type, so I think it does not actually signal the intent you’re claiming it does, it’s too broad a statement for what you’re trying to do, so type[T] is the proper way to spell this (although this still allows unions, so it’s not completely safe either, but it would at least be safe for the isinstance/issubclass use-case)

I agree it would be nice to have a short way to spell an alias for a valid runtime type (i.e. something that’s valid to use as a base class etc.), but that’s not what TypeAlias or the PEP695 type keyword is. For now you either have to redundantly spell the type or omit it and rely on inference.

1 Like

Well I guess we disagree. As you point out, that type[T] can be used to validly annotate unions makes it as unsafe as doing the same with a TypeAlias. If you want to use an alias or a type[T] annotated variable in place of an actual class name, don’t put incompatible things in it.

Either way, the change that PEP 695 makes is deprecating something that previously, and imho reasonably, worked fine.

How is it as unsafe, when Union is actually the only exception [1] and is supported by both isinstance and issubclass, one is clearly a lot less safe than the other.

TypeAlias encompasses a whole lot more than just Union, including string forward references.

I agree that type[Any] is almost as unsafe as TypeAlias, since it includes structural types, but the same cannot be said for type[Foo] where Foo is a nominal type.


  1. And only if every type in the union happens to be a subclass of the type used within the annotation on the LHS ↩︎

>>> SafeAlias = str
>>> SafeAlias()
''
>>> UnsafeAlias = str | int
>>> UnsafeAlias()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'types.UnionType' object is not callable

Yes, precisely my point.

Thanks for the feedback. Does anyone else have anything to add?

Could you say why? Does that also mean that you think un-hinted type aliases (PEP 613’s “implicit aliases”, e.g. SafeAlias = str in the example above) are also unreasonable? If not, how do they differ from explicit type aliases? (honest questions)

Here’s an example of why I think it’s strictly worse to use TypeAlias for this use-case rather than leave it unannotated: mypy Playground

1 Like

For some further context, my mental model of a “type alias” is “something that can stand in place for a type”, irrespective of context. This is something that holds true of every other programming language I have used.

PEP 613’s implicit aliases (a variable assigned a type as a value) and explicit aliases (TypeAlias annotated variables) support this - sure you can also assign to it something that is not a valid class, but either static type checkers or the python runtime will notify you if you do.

As is PEP 695 aliases aren’t type aliases - they’re type annotation aliases, which isn’t the same thing. Which would be fine, except they claim to be type aliases, and have deprecated the only valid way of specifying an actual type alias.

Yes, mypy has some limitations. Lucky the python interpreter will catch that one. You have some unit tests that will catch that sort of thing if you accidentally type it, right?

Sure, but what do you get out of marking it as a TypeAlias if type checkers have better behavior for your use-case if you don’t? The purpose of TypeAlias is to resolve ambiguities and tell the type checker “yes, this will be used inside a type annotation”, which is not what you use it for, you use it as an alias for a nominal type, which is just a regular assignment in my book.

Making TypeAliasType work as a base class and inside isinstance/issubclass checks is not free, it requires additional logic, which does not seem worth the cost, given that most type aliases probably will not resolve to a valid base class anyways.

… being able to assign invalid values for a type alias to a type alias is not my use case.

So keep supporting TypeAlias for the uses cases that PEP 695 aliases doesn’t support if nobody wants to do that work? Don’t claim that PEP 695 type declarations are “type aliases”? Something else? Get creative, yo!

They both serve the same purpose, just because you were using it for something else doesn’t mean that an explicit wrapper object needs to transparently behave like a nominal type if all it contains is a nominal type.

The only reason why Foo: TypeAlias works the same as a regular assignment is because all it does is put TypeAlias into __annotations__["Foo"], at runtime it’s still just the value you assigned to it. Whereas the type keyword creates an instance of TypeAliasType.

That’s why I’m saying that the fact that this worked previously is entirely incidental. There was no reason to put in any extra work to generate runtime errors with TypeAlias and conversely there’s no real reason to put in the work to make TypeAliasType work as a nominal type.

Okay, well I don’t think I can restate any differently why I disagree with what you’re saying than what I have already said previously, so I won’t be replying to you any further.

Again, thanks for the feedback.

TypeAlias was invented by PEP 613. It’s listed behavior is what TypeAlias should be used for. Using it in places for types are runtime is not one of them.

You “misusing” the feature for something else is just an expression of Hyrum’s Law.

1 Like

Ah unfortunately if you read PEP 613 you’ll see that it’s exactly intended to support my use case:

This PEP formalizes a way to explicitly declare an assignment as a type alias.

Maybe a better way of stating the question is:

Do you make use of type aliases in the code that you write? Traditional assignments - StringAlias = str, explicit assignments StringAlias:TypeAlias = str or PEP 695 aliases type StringAlias = str?

If so, what do you use them for?

The way this is formulated implies that a type alias is something different from a regular assignment, and the TypeAlias annotation is a way to distinguish the two, as such it only needs to semantically support what a type alias is designed to be used for, it doesn’t need to support everything a regular assignment would, it just so happens that because you marked a regular assignment as a type alias, it’s still a regular assignment at runtime. A PEP695 type alias on the other hand is not a regular assignment. I think your logic is backwards.

You can think of TypeAlias as an intersection between the resulting type of the type expression and TypeAliasType, whereas a PEP695 type alias is only a TypeAliasType which contains a type expression.

1 Like