Introduce new syntax to create `NewType`

With PEP 695 accepted in Python 3.12, the soft keyword type can be used to create type aliases.

# Old: 
from typing import TypeAlias
Point: TypeAlias = tuple[float, float]

# New:
type Point = tuple[float, float]

However, the current way to create NewTypes involves importing from the typing module, and providing a name string, which is unintuitive:

from typing import NewType
OrderId = NewType("OrderId", int)

Unlike type aliases, NewTypes don’t have first-class syntax support.


Proposal

What if we reuse the soft keyword type to create NewTypes? This will allow for built-in support.


Possible Syntax

[1]

type OrderId(int)
  • The above makes it clear that OrderId is “subclassed” from int
  • Similar to class OrderId(int): pass, but without runtime overhead

[2]

from typing import NewType
type OrderId = NewType[int]
  • Reusing NewType, but without needing to specify the name string
2 Likes

I like [2], so it would be

  • type OrderId = int for alias
  • type OrderId = NewType[int] for NewType

They just look nice together.

At runtime, type X = Y is, I believe, equivalent to

X = TypeAliasType("X", Y)

So, for [2], TypeAliasType would need to be special-cased for when its second argument is NewType[...]. But that shouldn’t be a problem?

For completeness I’ll also mention this syntax:

type OrderId = NewType(int)

but no-one ever likes it when I propose this.

Adding new syntax is very costly because it requires changes to the Python interpreter and/or many classes of tools (linters, type checkers, stub generators, etc.). It also requires users to understand a new syntactic form, so it comes with a cognitive burden. Any such proposed change should have a very compelling motivation. I recommend starting with a clear and compelling problem statement before proposing a syntax change.

PEP 695 proposed a new type statement because type aliases can be generic. Since PEP 695 was proposing a new way to declare type parameters, it needed to address type aliases. Also, generic type aliases are frequently used incorrectly today. Users tend to omit the type arguments when using a generic because they don’t realize that type arguments are needed. The new type statement in PEP 695 helps to eliminate that confusion by making it more obvious that a generic type alias has one or more type parameters associated with it.

What problem would a new syntax for NewType solve? The use of NewType is relatively rare compared to the use of type aliases. NewType doesn’t support generics, and in my experience, users of NewType are not confused about how it is intended to be used. For those reasons, I don’t see a compelling need to provide a new syntax here. It would save a few keystrokes, but since NewType is not used very often and is a more obscure and advanced feature of the type system, I don’t think that comes close to meeting the bar required to justify a syntax change — or new special casing in type checkers.

You might be able to make a more compelling case if you could gather data that shows NewType is used frequently in current code bases, but I would be surprised if the data backed that up.

1 Like

A counterpoint would be that introducing brand new type OrderId = int syntax is much more involved than introducing type OrderId = NewType[int] syntax on top of the former one. (I’m not fond of the type OrderId(int) idea, so I’m not arguing about it here.)

I use NewType frequently enough for me the new syntax to be beneficial. Repeating type name is so awkward…

I agree with @erictraut that the bar for introducing a syntax change is high and that NewType probably isn’t commonly enough used to merit special syntactic support.


However if NewType were to merit syntactic support in the future, I might advocate that:

from typing import NewType
OrderId = NewType("OrderId", int)

be phrasable using both the new and type keywords as:

new type OrderId = int  # a NewType

which would align nicely with the similar type alias:

type OrderId = int      # an alias

The use of pseudo-expressions like NewType[int] or NewType(int) presented earlier in this thread seem confusing to me because they look like expressions on their own but are actually part of a larger syntactic form that just spans an entire line.

2 Likes

For me no new syntax is required, if the type can be sub-classed then should be interpreted as a NewType if it cannot then should be interpreted as an Alias:

type Url = str              # interpret this as NewType
type UrlList = list[Url]  # interpret this as an alias (TypeAliasType)

class Url(str): pass               # This is valid
class UrlList(list[Url]): pass   # This is not valid

I think in this way we don’t need any new syntax for supporting NewType without importing, but it’s true that it would change current behavior as it may raise warnings in static checkers.

I can’t imagine any instance where I would want to make a real alias over a non generic class, not even when the name of the class is very long (as it can be just renamed, or imported with other name).

Isn’t type already a built-in function? I wonder how well that would play with the soft keyword

It works fine, as you can see in Python 3.12 where type is a soft keyword:

>>> import builtins
>>> type type = builtins.type(type)
>>> type
type
>>> type.__value__
<class 'typing.TypeAliasType'>