Static Enforced Non generic Type alias?

From the pep 484, we have this code (now modernized):

type Url = str

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

But I didn´t find any reference of this aliases should/ are enforced in static analysis.

for instance i expect:

test: str = 'not_an_url'
smght = retry(test)  # warning

test = 'not_an_url'
smght = retry(test)  # maybe no warning based on --strict? 

test: Url = 'not_an_url'
smght = retry(test)  # no warning

Not sure if this is related with variance / contra-variance, but I think the usefulness of type alias is a bit diminished if they play no role at static analysis.

Is this the expected behavior? or does it exclusively depend on the choices of the static analyzer?

An alias is, by definition, a synonym.

>>> Url = str
>>> Url is str
True

If you want something that behaves like str but isn’t a str, you can subclass it.

class Url(str):
    pass

def retry(url: Url, retry_count: int) -> None: ...
test: str = 'not_an_url'
smght = retry(test)  # error: Argument 1 to "retry" has incompatible type "str"; expected "Url"
1 Like

That is a good observation, thanks! I thought type Url was already a sort of class for the static analyzer.

1 Like
>>> Url = str
>>> Url is str
True
>>> type Url = str
>>> Url is str
False

Hmm. I have to admit I am not sure what the difference is.

That result is very surprising, I always thought these were exactly the same thing:

type Url = str
Url: TypeAlias = str

But if they are not and type is indeed a ‘type’ and not just a synonym, then the question remains open.

In anyway the important thing is how they are seen at static analysis more than at runtime.

I believe you could also use typing’s NewType

As described on More types - mypy 1.8.0 documentation

5 Likes

With type Url = str, Url apparently becomes a TypeAliasType. Introduced in PEP 695, though there isn’t much in the way of rationale for why it’s needed.

The PEP does specify that TypeAliasType is a runtime thing, so I guess that explains why mypy doesn’t see it.

A limitation of NewType is that it’s not actually a type at runtime.

Url = NewType("Url", str)
myurl = Url("discuss.python.org")
isinstance(myurl, Url)  # TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

Sometimes that might be desirable, if one really only want the check to be static, but it’s worth keeping in mind.

1 Like

So the “type” keyword does an “alias” instead of a “newtype”? that seems very confusing syntax form me.

I think type should be treated as NewType at static analysis but kind of ignored or be a subclass at runtime.

To summarize:

Typechecking Runtime
Url = str Synonym Synonym
type Url = str Synonym Distinct
Url = NewType("Url", str) Distinct Synonym (kind of)
class Url(str): pass Distinct Distinct

There should be one-- and preferably only one --obvious way to do it.

5 Likes

Url = NewType("Url", str) Url is str => False
I think the table needs an update there haha, but then NewType is very similar to a class, which for me makes sense. And also (for me)the ‘type’ keyword should be the one doing that.

The only thing clear is that the zen of python is suffering with this topic haha,

NewType: The static type checker will treat the new type as if it were a subclass of the original type

So yes, NewType is what I’m looking for, a subclass with no/near zero runtime impact.

But then the question is why ‘type’ creates a TypeAliasType and not a NewType?

1 Like

If you want to create a type that is effectively a subclass of an existing class from the perspective of a static type checker, that’s the job of NewType. That facility has existed since PEP 484, and it works well. There’s no need to create a new syntax for it.

The type syntax was created in PEP 695 as a way to create a type alias. This is typically used when you want to define a short and concise name for a longer, more complex type (such as a union of other types). It’s also useful for creating generic type aliases and recursive self-referential) aliases. The result of a type statement is intended to be used in type annotations, not as a runtime value.

These are two different mechanisms in the type system intended to address different use cases. @Pablo, for the problem you’re trying to solve, you should use NewType.

3 Likes

What is the benefit of a type Url = str alias over a Url = str alias?

What is the benefit of a type Url = str alias over a Url = str alias?

The type statement unambiguously defines a type alias — a symbol that can be used in other type expressions (annotations).

The intent behind the statement Url = str is ambiguous because this syntax is used in Python for both defining a variable Url and for defining a type alias Url. Type checkers employ heuristics to distinguish between these two cases. These heuristic rules have never been spec’ed, which leads to inconsistencies between type checkers. This ambiguity even extends to the use of the resulting symbol, especially when generics are involved. Take for example MyList = list. If you later use MyList in a type annotation (x: MyList), should MyType be considered already specialized with an implicit type argument of Any, or should it be considered unspecialized, which makes x: MyList[int] legal? These ambiguities lead to problems for type checkers – and for code bases that are intended to work across type checkers.

This problem of ambiguity led to the introduction of the TypeAlias special form in PEP 613, which allows you to unambiguously indicate that your intent is to define a type alias rather than define a variable.

The type statement is an extension of the TypeAlias idea, but it accommodates the new type parameter syntax when defining generic type aliases. It also doesn’t require importing TypeAlias from typing, and it allows for recursive (self-referential) type aliases without the use of quotes.

In summary:

  • If your intent is to define a type alias that will be used in type annotations, I recommend using the type statement (or TypeAlias if your code must run on Python < 3.12).
  • If your intent is to define a variable that can be used within value expressions at runtime (such as a constructor call), then use the Url = str form.
  • If your intent is to define a “new class type” that acts as though it’s a subclass of another class for purposes of static typing, then use Url = NewType("Url", str).

Most languages that support static types clearly delineate between “value expressions” and “type expressions” and define separate syntax and semantic rules for each. In the early days of the Python type system, these concepts bled together in ways that cause many problems. We’ve been working to disentangle these concepts over the years, but this thread highlights the fact that these concepts is still unclear in the minds of many Python developers. I think we need to do a better job clarifying these concepts in the typing spec and related typing documentation.

5 Likes

I think this is very clarifying, many thanks!

Now I see that NewType behaves as a class for the static analysis while remaining low/no impact at runtime, that is what I was looking for. And also what I expect a type to do.

I guess the last question to clarify is why using the type keyword for an alias?

After this post I’m starting to think that these are very different concepts, so if i could, I would be tempted to use the keyword type for NewType and maybe, if needed using a new keyword alias or other for an alias.

Maybe the whole confusion is that, when I see NewType I naturally thing that is replaced by type as we already have changed Dict - dict, List - list etc in our code base.

During the design process for PEP 695, there was a healthy and open debate about the best keyword to use here. The word alias was considered, but we settled on type. This is the same keyword used in other languages for the same concept. There are obviously pros and cons to every choice.

1 Like

I see, thanks again!

Would be still possible to use type as NewType by inference?

If the alias is just one word:

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

I would justify this decision based on that the first can be used as a subclass and the second cannot.

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

Also because otherwise I can barely see the value of this:
type Url = str

Anything is possible if you can convince the typing community and the Typing Council to amend the typing spec, but I think it’s very unlikely that you would be able to make a convincing argument for such a change. First, it would be a breaking change, so it would break existing code bases that use type as it was spec’ed in PEP 695. Second, it would create ambiguity in a feature that was designed to eliminate ambiguity. So practically speaking, I think the answer to your question is “no, this isn’t possible”.

NewType is a very different concept than a type alias. I think it’s best for people to internalize that fact and use the construct that applies to their particular use cases.

3 Likes