Use type keyword for defining NewTypes in Non-Generic types

Now type aliases for Non-generic types are almost useless, like the one showed in the PEP 484

Url = str
def retry(url: Url, retry_count: int) -> None: ...

With the modern syntax, we would have:

type Url = str
def retry(url: Url, retry_count: int) -> None: ...

Which won’t do anything in static analysis:

test: str = 'not_an_url'
test1 = retry(test)  # Doesn't give a static warning

test = 'not_an_url'
test2 = retry(test)  # Doesn't give a warning, but may be nice based on --strict

test: Url = 'not_an_url'
test3 = retry(test)  # Doesn't give a static warning (It shouldn't)

So in my opinion we are better off just omitting the alias, and ensure that the variable name is descriptive. Or better use typing.NewType.

def retry(url: str, retry_count: int) -> None: ... # option 1

Url = NewType("Url", str)
def retry(url: Url, retry_count: int) -> None: ... # option 2

Given that I can’t see any use-case of an alias in a Non-generic type, please tell me if I’m missing something. My proposal would be, using the keyword type for defining Newtypes in Non generics:

type Url = str              # interpret this as NewType , it can be subclassed
type UrlList = list[Url]    # interpret this as Alias (TypeAliasType),   it can't be subclassed

Considering that one can be sub-classed and the other not:

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

This behavior will be more in line with what I would expect when using the word “type”. And also gives a real usage to the keyword type with non-generic classes.

PD: This topic was already discussed here #45186, where I questioned this behavior, before i figured out how and why it works like that. This post intends to be a proposal after having a slightly deeper understanding on the topic. In any case if is too similar for the rules of the forum, feel free to close it or tell me to do so.

They are very useful, as the ~1200 type aliases – of which only a small number is generic – in typeshed show.

2 Likes

Incorrect, those are both valid.

If I understand you correctly, you are asking for a type alias facility that works by name equivalence, unlike assignment aliases which work by structural equivalence. I suggest you read up on those concepts. Wikipedia seems to be using different words for it: Nominal type system - Wikipedia and Structural type system - Wikipedia - maybe terminology has changed over time.

It’s an age old discussion which is better. I think the world mostly settled on structural equivalence. The problem with name equivalence is that you keep ending up in situations where you need an Url, but all you have is a str, and then you end up casting all the time.

Ok, thanks everyone, now I see more things that I though were not possible:

class UrlList(list[str]): pass  # this is indeed valid.

Also the difference between Structural and nominal types systems is very illustrative, as well as the observation the that usage of NewType may force casting as well.

I guess i could close this Idea, because even if we would want (that won’t be the case) is actually impossible to implement, cause everything can be sub-classed.