Clarifying runtime usage of `TypeAlias`es

Neither mypy nor pyright report an error here, so maybe there’s something to this:

from typing import TypeAlias

Foo: TypeAlias = "int"
Bar: TypeAlias = str

def f() -> None:
    a = Foo()  # no issues reported, a is inferred as `int`
    b = Bar()  # no issues reported, b is inferred as `str`

Allowing Foo() clearly seems like a bug to me. But what about Bar? In my mind, TypeAlias has the same meaning as the type statement, so I think it doesn’t make sense to instantiate a type alias or call class methods on it, even if it happens to refer to a class.

On the other hand, some codebases (e.g.: Run-time behaviour of `TypeAliasType - #87 by tibbe) might rely on this. So maybe my understanding is incorrect, and TypeAlias was intended to be used like that? In that case, would changing an exported : TypeAlias to a type statement, or to a stringified annotation, in a library be considered a breaking change?

6 Likes

Allowing either seems wrong to me, but the “correct” use in my mind requires something we dont have currently with TypeForm (which is still being worked on).

I think ideally, explicit type aliases gain an as_value method (or explicitly document use of .__value__ for this purpose) to explicitly unwrap them for use as a value, and if people are discontent to wait for 3.12(?) 3.15(?) (depending on __value__ vs as_value) to be the minimum supported python, a function in typing extensions type_as_value serving the same purpose. The function would have to be documented as resolving forward references for this to work.

Without the TypeForm proposal, these would need special casing to have the semantics of TypeForm for typecheckers, but I place a pretty significant value on being able to statically distinguish SomeType being an expression of “not exactly this type, but anything assignable to it under the typesystem rules” and “The type object SomeType, that when initialized returns an object of SomeType”

In rust there are two ways to alias a type:

struct MyStruct(u32);

use MyStruct as UseAlias;
type TypeAlias = MyStruct;

let _ = UseAlias(5); // OK
let _ = TypeAlias(5); // Doesn't work

There is an intentional difference between these two approaches. The use ... as ... is similar to Python’s TypeAlias and behaves transparently at runtime. And clearly the type _ = ... in rust behaves similar to Python’s type _ = ... keyword, i.e. as an opaque static type.

For details, see Type aliases - The Rust Reference


I think that stringified TypeAlias (and only those) should fall in the “opaque” category, even though it might not look that way to type-checkers at the moment. Because after all, the task of type-checkers is to prevent bugs, preferably without being needlessly restrictive.

Anyway, to make a long story short; here’s what I think:

  • Calling TypeAliasType instances (type _ aliases) results in an error at runtime, so type-checkers should reject it.
  • Foo() (i.e. "int"()) will result in an error at runtime, so type-checkers should reject it.
  • Bar() (i.e. str) is perfectly fine at runtime, so type-checkers should allow it.

I think an as_value being either a property or method of type aliases seems wrong. You’d always have to either check via isinstance or hasattr, wether the .as_value exists (or .__value__).

Therefore I think a typing.evaluate_alias(T) should be added to typing. If the given argument is a type or ForwardRef, it just returns that, otherwise it evaluates the value and returns that.

2 Likes