`Literal` and PEP 695 type aliases

Currently, the typing spec does not say anything about the following:

# Should we allow this?
type TestType0 = Literal["test1", "test2"]
type TestType1 = Literal[TestType0, "test3"]

assert_type(TestType1, Literal["test1", "test2", "test3"])

It seems that both pyright and mypy allow this currently. Unfortunately, the type alias isn’t expended at runtime (TestType1.__args__ != ("test1", "test2", "test3)).

Should we update the typing specification for literals to support this use case?

Seems like pyright and mypy also allow the following:

type TestType0 = Literal["test1", "test2"] | None
type TestType1 = Literal[TestType0, "test3"]

reveal_type(TestType1)
# with pyright: Literal['test1', 'test2', 'test3'] | None
# (using reveal_type on PEP 695 type aliases doesn't work with mypy) 

I’m wondering if this is semantically correct? Edit: it is, as described at the end of this section.

Crossref: Nested Literals together with PEP-695 type statement are failed to produce json schema · Issue #9269 · pydantic/pydantic · GitHub

I think the typing spec already covers this case. It indicates that the RHS of a type statement must be a valid type expression, and it says that “Literal may also be parameterized by … type aliases to other literal types”. That means the definition of TestType1 in your example is valid and has a well-defined meaning according to the spec.

At runtime, TestType1 is an object of type TypeAliasType, so your assert_type fails. If you replace it with assert_type(TestType1, TypeAliasType), the assert will succeed.

In general, you shouldn’t use a type expression as the first argument to either assert_type or reveal_type. These functions are intended to operate on value expressions. If you do provide a type expression, you’re effectively asking the type checker to evaluate the type expression as a value expression. Many type expressions evaluate to undocumented types and there is no specification for how static type checkers should evaluate these expressions.

If you use the type alias TestType1 to annotate a variable or parameter symbol and then pass that symbol as the first argument to assert_type, your current formulation will succeed.

def func(x: TestType1):
    assert_type(x, Literal["test1", "test2", "test3"])

In summary, I think the typing spec already covers this case, and both mypy and pyright are conforming to the spec. Based on the linked discussion, it appears that pydantic is not correctly handling the TypeAliasType object in this case.

3 Likes

Ah yes, I used assert_type in my example as a way to show what was the inferred type by pyright, i.e. when hovering on it:

image

Perhaps it is worth adding an example using the new type syntax then? It might be surprising at runtime as unlike old style aliases/assignments, the __value__ of a type alias won’t be flattened.