Aliases keep signatures readable; NewType prevents mixing up values that share a runtime type.
A type alias gives a long type a name. Python 3.12 added the type statement:
type JSON = dict[str, JSON] | list[JSON] | str | int | float | bool | None
type Pair[T] = tuple[T, T]The value is evaluated lazily, so aliases can be recursive (like JSON) or refer to classes defined later, and they can be generic. Older code writes Vector: TypeAlias = list[float] or a plain assignment; both still work.
An alias is interchangeable with what it names. If you want the checker to distinguish two things that are both int at runtime, such as user IDs and order IDs, use NewType:
UserId = NewType("UserId", int)UserId(5) is just 5 at runtime (the call is nearly free), but the checker treats UserId as a subtype of int: you can pass a UserId where an int is expected, but not a plain int or an OrderId where a UserId is expected. That catches swapped-argument bugs.
You can't subclass a NewType or use it with isinstance; it isn't a real class.
Example
from typing import NewType
type Matrix = list[list[float]]
type Tree = dict[str, Tree] | None # recursive alias
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def cancel(user: UserId, order: OrderId) -> str:
return f"user {user} cancels order {order}"
def trace(m: Matrix) -> float:
return sum(m[i][i] for i in range(len(m)))
uid, oid = UserId(7), OrderId(1001)
print(cancel(uid, oid)) # user 7 cancels order 1001
print(type(uid), uid + 1) # <class 'int'> 8
print(trace([[1.0, 2.0], [3.0, 4.0]])) # 5.0
print(Matrix.__value__) # list[list[float]]
# cancel(oid, uid) -> checker error: "OrderId" is not "UserId"Remember
- type Name = ... declares a lazily evaluated, possibly generic alias
- Aliases are interchangeable with the aliased type
- NewType creates a distinct checker-only subtype at almost zero runtime cost
Watch out for
- Expecting isinstance(x, UserId) to work; NewType isn't a class.