In many type aliases, a union is the outermost operation.
The recent PEP 846 (docstrings for type aliases) reminded me of an old idea of mine. That PEP proposes to attach a string statement to the preceding type statement, because the current type statement (unlike class) has no natural place for a docstring. I propose the following class-like syntax that takes the union of all its members.
For example, instead of:
type Timeout = int | float | None
"""Maximum wait in seconds; None waits indefinitely."""
we could have:
type Timeout:
"""Maximum wait in seconds."""
int
float
None # waits indefinitely
That is the whole proposal.
One more example from typeshed’s inspect.pyi, which today looks like:
type SourceObject:
"""Objects whose source inspect.getsource() and friends can retrieve."""
ModuleType
type[Any]
MethodType
FunctionType
TracebackType
FrameType
CodeType
Callable[..., Any]
Why I think this is worth considering:
Union is often the outermost structure of a type alias. In that case, indentation can express this structure directly and cleanly.
Long unions are common. The example is not an outlier: typeshed’s stdlib stubs contain 55 union aliases with four or more members (up to an extreme of 16, as in _Marshallable).
Documentation gets a natural home. A docstring at the top of a suite feels Pythonic.
Python used to be a language to “fint into our brains” - while it has since grown, I don’t see the need to include new syntax for a feature that is deemded optional - less so when it is covered by regular use of the existing syntax and whitespace.
The re-use of `type` for overloading it with yet a 3rd different meaning if something I still consider as harmful in itself.
Of course, it could also contain a single type, exactly as in @sinoroc example.
In that case, the main role of the class-like form would simply be to provide an alternative spelling of a TypeAlias with a natural place for a docstring, rather than to express a union.
type ConvertibleToInt:
"""Anything that can be passed to the int constructor."""
str
ReadableBuffer
SupportsInt
SupportsIndex
if sys.version_info < (3, 14):
SupportsTrunc
type ConvertibleToInt = (
str |
ReadableBuffer |
SupportsInt |
SupportsIndex |
(SupportsTrunc if sys.version_info < (3, 14) else Never)
)
"""Anything that can be passed to the int constructor."""
That was in the context of the thread proposing C-like variable annotation syntax such as int foo = 10. That’s what I meant when I said I disliked type name = value.
I later clarified this because someone else had the same confusion, evidently my phrasing was pretty poor!
As for the type statement itself, I have no issues with the current syntax. It also seems to be the convention in several other modern languages:
Rust: type Result<T> = ...;
TypeScript: type Result<T> = ...;
Go: type Result[T any] = ...;
Python: type Result[T] = ...