You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Type aliases without implicit conversion ("NewType") #1284
Several users (@wittekm among them) have asked for a feature where you can define something that's like a type alias in referring to an existing underlying type at runtime, but which isn't considered equivalent to it in the type-checker -- like what Hack and Haskell call newtype.
One classic application of this would be aliases/newtypes of str (or unicode or bytes) to distinguish fragments of HTML, Javascript, SQL, etc., from arbitrary text and enforce that conversions are only done with proper escaping, to prevent classes of vulnerabilities like XSS and SQL injection. A definition might look like HtmlType = NewType("HtmlType", str).
Other classic uses include distinguishing identifiers of different things (users, machines, etc.) that are all just integers, so they don't get mixed up by accident.
A user can always just define a class, say an empty subclass of the underlying type, but if an application is handling a lot of IDs or text fragments or the like, it costs a lot at runtime for them to be some other class rather than actual str or int, so that that isn't a good solution.
The main open question I see in how this feature might work is how to provide for converting these values to the underlying type. For a feature like this to be useful there has to be some way to do that -- so it's possible to write the conversion functions that take appropriate care like escaping text into HTML -- but preferably one that's private to a limited stretch of code, or failing that is at least easy to audit with grep. In Hack the types are implicitly equivalent just within the source file where the newtype is defined; in Haskell the newtype comes with a data constructor which is the only way to convert, and typically one just doesn't export that from the module where it's defined.
The Hack solution could work, and has the advantage that it means no run-time overhead at all, other than invoking the intended conversion functions that live in the newtype's file. It feels odd to me, though, because Python generally doesn't treat specially whether a thing is defined in a given module vs. imported into it. Another solution could be something like html = HtmlType.make(text) and text = HtmlType.unmake(html), which would follow perfectly normal Python scoping and would be reasonably auditable.
@gvanrossum, @ddfisher, and I discussed this for a while in person. The use case we understand relatively well is for things like user IDs, where one rarely wants to manipulate them as the underlying type (like doing arithmetic.) For things like HTML fragments, we'll want to understand the use case better.
Even for user IDs, one thing to be careful of getting right is equality -- you need to be able to write if user_id == requester_user_id: or whatever, so you need at least that one operation. And you probably want an error if someone ever compares a user ID to a plain int.
It'll also be important to make a newtype like this possible to gradually adopt in an already-annotated codebase -- so that if you have a bunch of code already annotated with int for the user IDs, you can go back and annotate a swath of it at a time with UserId instead. David suggested having a feature like UncheckedUnion, which is sort of like Any but narrowed to the specific types in the union; then one could write e.g. UncheckedUserId = UncheckedUnion[UserId, int], and at the boundary between UserId code and still-int code type things as UncheckedUserId. That saves having to write a bunch of casts, and it means no runtime effect.
I was playing with this the other day and had the idea to use classes that the type checker would see, but which would never be instantiated at runtime. It kind-of works :-)
The classes are only there to introduce types for the type checker — at runtime there is only ints. The __new__ methods lets you use the class when creating a new value. The type checker apparently expects that __new__ returns the cls passed in, so c = Celsius(123) will conveniently make the type checker assign the type Celsius to c. Alternatively, you could have used cast(Celsius, value).
This correctly flags the second call to is_it_boiling where the argument was in Fahrenheit, not Celsius.
However, since both classes inherit from int, they can be used in mixed expressions like
x=Celsius(10) +Fahrenheit(20)
That does not give an error since the type checker sees that as a call to int.__add__(other: int) and Fahrenheit is a subclass of int.
To get around that, I tried removing the common base class. That looks like this:
The Fahrenheit class would need similar boilerplate, which one might be able to put in a generic base class. With these definitions, a line like
x=Celsius(10) +Fahrenheit(20)
is flagged with
error: Argument 1 has incompatible type "Fahrenheit"; expected "Union[Celsius, Number]"
The use of Union[..., int] allows you to do comparisons with normal integers and you can thus save on the number of Celsius(...) wrappers needed. (I had originally thought I could use numbers.Number instead of int, but that doesn't work for some reason.)
So, the basic idea was: make a new type unrelated to all other types for each newtype. The new type gets the special methods needed to behave like the base type it is an alias for. At runtime, the new type is never instantiated — so there's no performance penalty due to an extra layer of indirection.
Since the types are not actually used at runtime, isinstance checks will fail (isinstance(Celsius(123), Celsius) is False since c is an int at runtime).
We very much would like to start using mypy, but since most of our codebase is just very nasty python3 code (none of them have stubs so far, okay maybe requests), so the important parts would be the "business logic", and those parts would mostly benefit from these custom value types. Though using classes works, and they only have a negligible memory overhead, they are a bit distracting (especially since developers have to be conscious of them to not use them for anything else than annotating/hinting).
This has been a fairly frequently requested feature. The main challenge is that there are multiple potential, different use cases, and we don't understand how important all of these are. Supporting all of the possible use cases might be hard, and almost certainly not a good idea anyway.
Here's a list of things which might be useful to support (some of these could be worthless):
Create a very limited type for things like IDs that is separate from other types but is represented at runtime as an int/str or similar. It wouldn't inherit most operations from the target type. Currently defining a dummy class as proposed above can be used as a workaround, and we could easily provide a better syntax for this. Type checking of == operations is still an open issue, as mypy allow arbitrary objects compared for equality.
Create something that provides a (tweaked) subset of operations supported by int or another type, but is represented at runtime as int (or whatever). The Celsius example above is like this.
Create something that behaves more or less like a subclass of int/str but is a regular int/str at runtime. This already kind of works via a __new__ hack, but it's not quite perfect as binary operations will likely have wrong result types by default (e.g. MyInt() + MyInt() is int instead of MyInt).
Create something that behaves exactly like another type such as str but that is not compatible at all with the another type. For example, create an alias Path for str, and don't support mixing Path and str objects in operations at all. Also, Path would not be a subtype of str. However, Path() + Path() would be okay.
Use cases 1, 2 and 4 could be implemented via wrapper objects, but this would have extra memory overhead, and potentially also speed impact. Also, use case 4 would potentially require replicating the entire interface of the target class, resulting in a lot of boilerplate code. However, use case 4 seems pretty marginal. Currently use case 3 isn't properly supported, as far as I can see.
My guess is that use case 1 is reasonably common, but the others are less so.
For start simply the ability to define a type, while avoiding the use of class, without any primitive type semantics would be okay.
Usually the fact that a user_id is an integral type (or that it's numeric at all) doesn't matter. It's just a unique id for lookups and update access. And it'd help catch a lot of bugs due to accidentally calling a function with the wrong arguments, returning the user name instead of the id and so on. (Or maybe a better example is user name vs. email address, both are usually represented as simple (base)string typed variables/values, yet semantically they are completely different.)
user_controller.py
some HTTP controller, whose actions can only take in primitive arguments. Its only purpose in life is to call into smarter logic like the above user.py, and maybe some validation.
fromuserimportUserId, User# just use a type: comment to say 'hey, I want to treat user_id_as_int as a UserId 'defget_user_name(user_id_as_int):
# type: (int) -> struser_id=user_id_as_int# type: UserId# the above would fail if 'user_id_as_int' were not an int.# could probably also be accomplished by a cast().returnUser.get_by_user_id(user_id).name
The basic rules I could foresee would be:
RULE 1: functions that accept an int, can also accept a UserId
as exemplified by db.hey_this_function_accepts_an_int in get_by_user_id - the passed in variable is a UserId.
Highly simplifies migrations to a TypeAlias-friendly setup.
RULE 2: functions that accept a UserId need to have their parameters explicitly identified as a UserId, through either a #type: or a cast().
as exemplified in the controller.
forces the person consuming the library to say, "oh geez, okay, i promise you that this is a UserId".
[sidenote for if this proposal does go through: this will probably be used for IDs a lot. You should publish some opinion like the mypy authors think capitalizating like 'UserId' is more idiomatic casing than 'UserID', or the other way around. The most annoying thing in the world would be if somebody had a TypeAlias called UserID and another guy had a type alias called EmailId.]
Then hinting these callbacks (and other dynamically loaded and utilized Callables) seems like a big selling point of mypy, but in this case I wasn't able to come up with a class that somehow subtypes this type. (Probably because I don't know enough about how special the code in typing.py is, or maybe just because Callable is Final.) And of course it's unlikely that there are many similarly strange/ugly typed variables (functions), though there are a lot of uses of callable values with simpler types.
For example Flask and Werkzeug use class valued variables (response_class, app_ctx_globals_class and others), and they could be typed as Callable[[], Any]s, because due to duck typing the actual classes instantiated by the framework doesn't have to implement anything. (Or even if Flask uses ABCs, some other code might not.)
(I'll respond to @wittekm's example and rules in a separate comment.)
@PAStheLoD: Callable is indeed final, you can't subclass it. It's about as special as Union or Tuple. And type-checking of Callable types is entirely structural, since that's what "callable" means fundamentally. (In fact it's a lot more fundamental than "int" or "str".)
I think what you're trying to do here is the following.
You have an API that takes a callback that implements some policy (e.g. for caching). There may be some predefined policies you are also fine with users implementing their own policies. However, you want them to explicitly state when they are writing a policy, so that only functions explicitly marked as policy functions are accepted as policy parameters in your APIs.
In a dynamic world, you could easily do this by requiring a @policy decorator that just adds a special _policy_ attribute to the function object, and in the API you check the policy callbacks for that attribute. But you wish that you could let mypy do this for you -- users would still add a @policy decorator, but that decorator would subtly modify the type, and your policy API arguments would be declared as only accepting such a policy. Then mypy would catch mistakes without runtime overhead.
I think this is an interesting if slightly esoteric use case, and it looks possible that the solution onto which we are converging for simpler type aliases will made to work for callables too. For example:
# Library codeT=TypeVar('T')
_Policy=Callable[[Any, str, Any, T], Tuple[Optional[T], Optional[T]]] # The raw signaturePolicy=TypeAlias('Policy', _Policy) # The marked typedefmake_policy(func: _Policy) ->Policy: # The decoratorreturncast(func, Policy) # Maybe the cast isn't even neededdefsome_api(p: Policy) ->None:
<thisonlyacceptsfunctionsmarkedwith @Policy># User codefromlibraryimportT, Policy, make_policy, some_api@make_policydefmy_policy(a, b: str, c, d: T) ->Tuple[Optional[T], Optional[T]]:
<implementapolicy>defnot_a_policy(a, b: str, c, d: T) ->Tuple[Optional[T], Optional[T]]:
<lookslikeapolicybutisn't>some_api(my_policy) # OKsome_api(not_a_policy) # Error (from mypy!)
@wittekm: I have a few questions about the details of your proposal.
(I also edited your example a bit to correct obvious typos, like a missing self for the name property and to make the get() call correspond to the definition of get_by_user_id(). I hope that's OK.)
You are proposing asymmetric rules: any function that takes an int will silently accept a UserId, while any function that takes a UserId won't take an int. (In mypy -- at runtime they both accept either type since UserId is just int at runtime.)
A slight problem with this is that it seems to allow int operations on UserId instances, since operations are just syntactic sugar for functions. So this code would still be allowed:
defget(uid: UserId) ->None:
x=uid+1# Has type int, but makes no sense<dosomethingwithx>
I'm not sure if this is a show-stopping deficiency or something that we can live with. It seems to go against Jukka's (1) from #1284 (comment), where he says that the alias wouldn't inherit most operations. I'm curious if we can define a rule that actually follows (1). Certainly some operations should still be allowed, e.g. __str__ or __eq__. How would we define the set of operations that TypeAlias() strips without special-casing e.g. int? This is essentially the problem from #1284 (comment) (the example with Celsius and Fahrenheit). We really need to decide about this before we can move forward.
My other question is about using cast() vs # type comments. Your example has
user_id=user_id_as_int# type: UserId
and you wrote (both in the comment and in RULE 2) that it might also possibly use a cast. I would really hope we won't need to use or encourage casts here, since it's an expensive runtime operation (at least it is until we teach CPython about it -- right now it is a user-defined function, which is incredibly slow compared to a plain assignment). Unfortunately (and here @JukkaL might know better) I think that there's not much difference between the above and
Use.get_by_user_id(user_id_as_int)
since in both cases mypy sees an int (user_id_as_int) in a context where a UserId is required. To understand this it helps to realize that putting a # type comment on an assignment is really a crutch for a variable declaration -- in some hypothetical future Python syntax the above example might be written as
varuser_id: UserId=user_id_as_int
IOW the # type comment just gives the type of the variable, not of the expression, and mypy then has to decide whether the expression's type is acceptable for the variable's type. There really can be only a single rule to decide whether an expression's type is acceptable for a given context (either a variable or argument supplies a context).
However, all that seems to point us in the direction of requiring a cast, and as I said I really don't like that. How can we formulate a rule that does what we want? You tried with RULE 1 and RULE 2 but I think we'll need some additional subtlety that eludes me. Maybe we need to distinguish between different kinds of contexts (e.g. assignment being different from arguments) and allow conversion of int to UserId in one context but not another.
Going back to @JukkaL's list of use cases (1)-(4) and the Celsius/Fahrenheit example, I would also like to discuss runtime costs more. Some use cases require defining a new class using standard Python class syntax (e.g. class Celsius(int): ...). There's no way that can have zero runtime cost, and I think if we're going that road it's not worth also adding special handling to mypy. The thing that's exciting would be to have a TypeAlias() special form that disappears entirely at runtime. But then I want to avoid the need for casts too. Here I am getting a headache. :-)
Finally. Once we agree on how to do it I think we should come up with a better name, since "type alias" in PEP 484 is already used for pure aliases that aren't type-checked -- they are just shorthands to avoid having to write the same thing over and over.
Sorry for the rambling!
PS. Re: capitalization of UserID vs. UserId, I find the latter looks better. PEP 8 has something to say about abbreviations like HTTP (it prefers HTTPServerError), but I think that's really more about initialisms like HTTP (== HyperText Transfer Protocol), while to me ID feels more like a shortening of "Identity". I could see HTTP being evolved from H.T.T.P., but writing I.D. for Identity makes no sense -- even though I've seen it.
@gvanrossum: Yes, thank you, that's the broadest use case I had in mind, sans the decorator, as I was envisioning that marking a function as this "non-equal alias" type would be enough information (both for the developer and mypy).
@gvanrossum: You are correct, mypy uses the same rules for assignment and functions calls. Making assignments special wouldn't be too hard technically, but this special casing feels pretty ad-hoc to me. Users might expect that an assignment with a type annotation can be used as a cast in general and would get confused as it would only work for certain types. Using a type alias would still have a runtime cost (an extra assignment statement), and I'm not yet convinced that the runtime overhead of a call is significant enough to make the type system less consistent.
Instead of using cast or an assignment, there are other options we could pursue. Here are a few ideas.
A) Make the alias a callable object
This should be slightly more efficient than a cast as there is only a single argument. This is arguably more readable and less error-prone that with a cast:
We'd add a new kind of type, but we wouldn't need to touch the semantics of assignments. The idea is that a special type would accept int as a value, but the type would still be a subtype of UserId. This is similar to the unchecked union proposed by @ddfisher, but I'd rather use a different name for this. I don't have a great proposal for the name, though. The code would be efficient as we don't need an assignment or a call.
Example:
from typing import NewType, Implicit
UserId = NewType('UserId', int)
def get_user_name(user_id):
# type: (Implicit[UserId]) -> str
return User.get_by_user_id(user_id).name # ok
get_user_name(2) # ok
The type Implicit[UserID] would allow UserId or int, but maybe it wouldn't allow other aliased types such as FileId (even if that is internally an int). It would be compatible with UserId -- it's basically a bridge between int and UserId. Implicit[t] probably isn't the best way to phrase this. Some other ideas, none of which seem very intuitive:
I don't even know if this PEP change will go through, or when (even though I have it in the 3.5.2 milestone for the PEP -- that just means I want to think about it).
So the PEP work on this feature (spec in pep-0484.txt and runtime support in typing.py) is complete (python/typing#226) -- all we need is the mypy implementation now!
Activity
gnprice commentedon Mar 10, 2016
@gvanrossum, @ddfisher, and I discussed this for a while in person. The use case we understand relatively well is for things like user IDs, where one rarely wants to manipulate them as the underlying type (like doing arithmetic.) For things like HTML fragments, we'll want to understand the use case better.
Even for user IDs, one thing to be careful of getting right is equality -- you need to be able to write
if user_id == requester_user_id:or whatever, so you need at least that one operation. And you probably want an error if someone ever compares a user ID to a plain int.It'll also be important to make a newtype like this possible to gradually adopt in an already-annotated codebase -- so that if you have a bunch of code already annotated with
intfor the user IDs, you can go back and annotate a swath of it at a time withUserIdinstead. David suggested having a feature likeUncheckedUnion, which is sort of likeAnybut narrowed to the specific types in the union; then one could write e.g.UncheckedUserId = UncheckedUnion[UserId, int], and at the boundary betweenUserIdcode and still-intcode type things asUncheckedUserId. That saves having to write a bunch of casts, and it means no runtime effect.mgeisler commentedon Mar 12, 2016
I was playing with this the other day and had the idea to use classes that the type checker would see, but which would never be instantiated at runtime. It kind-of works :-)
At first I tried:
The classes are only there to introduce types for the type checker — at runtime there is only
ints. The__new__methods lets you use the class when creating a new value. The type checker apparently expects that__new__returns theclspassed in, soc = Celsius(123)will conveniently make the type checker assign the typeCelsiustoc. Alternatively, you could have usedcast(Celsius, value).This correctly flags the second call to
is_it_boilingwhere the argument was in Fahrenheit, not Celsius.However, since both classes inherit from
int, they can be used in mixed expressions likeThat does not give an error since the type checker sees that as a call to
int.__add__(other: int)andFahrenheitis a subclass ofint.To get around that, I tried removing the common base class. That looks like this:
The
Fahrenheitclass would need similar boilerplate, which one might be able to put in a generic base class. With these definitions, a line likeis flagged with
The use of
Union[..., int]allows you to do comparisons with normal integers and you can thus save on the number ofCelsius(...)wrappers needed. (I had originally thought I could usenumbers.Numberinstead ofint, but that doesn't work for some reason.)So, the basic idea was: make a new type unrelated to all other types for each newtype. The new type gets the special methods needed to behave like the base type it is an alias for. At runtime, the new type is never instantiated — so there's no performance penalty due to an extra layer of indirection.
Since the types are not actually used at runtime,
isinstancechecks will fail (isinstance(Celsius(123), Celsius)isFalsesincecis anintat runtime).PAStheLoD commentedon Mar 18, 2016
Hello!
Excuse me for not bringing much except a long
+1.We very much would like to start using mypy, but since most of our codebase is just very nasty python3 code (none of them have stubs so far, okay maybe requests), so the important parts would be the "business logic", and those parts would mostly benefit from these custom value types. Though using classes works, and they only have a negligible memory overhead, they are a bit distracting (especially since developers have to be conscious of them to not use them for anything else than annotating/hinting).
Thanks for your efforts!
JukkaL commentedon Mar 18, 2016
This has been a fairly frequently requested feature. The main challenge is that there are multiple potential, different use cases, and we don't understand how important all of these are. Supporting all of the possible use cases might be hard, and almost certainly not a good idea anyway.
Here's a list of things which might be useful to support (some of these could be worthless):
int/stror similar. It wouldn't inherit most operations from the target type. Currently defining a dummy class as proposed above can be used as a workaround, and we could easily provide a better syntax for this. Type checking of==operations is still an open issue, as mypy allow arbitrary objects compared for equality.intor another type, but is represented at runtime asint(or whatever). TheCelsiusexample above is like this.int/strbut is a regularint/strat runtime. This already kind of works via a__new__hack, but it's not quite perfect as binary operations will likely have wrong result types by default (e.g.MyInt() + MyInt()isintinstead ofMyInt).strbut that is not compatible at all with the another type. For example, create an aliasPathforstr, and don't support mixingPathandstrobjects in operations at all. Also,Pathwould not be a subtype ofstr. However,Path() + Path()would be okay.Use cases 1, 2 and 4 could be implemented via wrapper objects, but this would have extra memory overhead, and potentially also speed impact. Also, use case 4 would potentially require replicating the entire interface of the target class, resulting in a lot of boilerplate code. However, use case 4 seems pretty marginal. Currently use case 3 isn't properly supported, as far as I can see.
My guess is that use case 1 is reasonably common, but the others are less so.
PAStheLoD commentedon Mar 18, 2016
For start simply the ability to define a type, while avoiding the use of
class, without any primitive type semantics would be okay.Usually the fact that a
user_idis an integral type (or that it's numeric at all) doesn't matter. It's just a unique id for lookups and update access. And it'd help catch a lot of bugs due to accidentally calling a function with the wrong arguments, returning the user name instead of the id and so on. (Or maybe a better example is user name vs. email address, both are usually represented as simple (base)string typed variables/values, yet semantically they are completely different.)gvanrossum commentedon Mar 18, 2016
Could you work out a complete example of how that would look?
wittekm commentedon Mar 19, 2016
(I'm not Pas, but here's my two cents for an example:)
user.py
This is a model. Maybe some business logic.
user_controller.py
some HTTP controller, whose actions can only take in primitive arguments. Its only purpose in life is to call into smarter logic like the above
user.py, and maybe some validation.The basic rules I could foresee would be:
RULE 1: functions that accept an int, can also accept a UserId
db.hey_this_function_accepts_an_intinget_by_user_id- the passed in variable is a UserId.RULE 2: functions that accept a UserId need to have their parameters explicitly identified as a UserId, through either a #type: or a cast().
[sidenote for if this proposal does go through: this will probably be used for IDs a lot. You should publish some opinion like the mypy authors think capitalizating like 'UserId' is more idiomatic casing than 'UserID', or the other way around. The most annoying thing in the world would be if somebody had a TypeAlias called UserID and another guy had a type alias called EmailId.]
PAStheLoD commentedon Mar 19, 2016
Thanks to wittekm's detailed example all I have to add is an other use case where type equivalence can cause problems.
So let's say we have this:
Then hinting these callbacks (and other dynamically loaded and utilized Callables) seems like a big selling point of mypy, but in this case I wasn't able to come up with a class that somehow subtypes this type. (Probably because I don't know enough about how special the code in typing.py is, or maybe just because Callable is Final.) And of course it's unlikely that there are many similarly strange/ugly typed variables (functions), though there are a lot of uses of callable values with simpler types.
For example Flask and Werkzeug use class valued variables (response_class, app_ctx_globals_class and others), and they could be typed as
Callable[[], Any]s, because due to duck typing the actual classes instantiated by the framework doesn't have to implement anything. (Or even if Flask uses ABCs, some other code might not.)gvanrossum commentedon Mar 20, 2016
(I'll respond to @wittekm's example and rules in a separate comment.)
@PAStheLoD: Callable is indeed final, you can't subclass it. It's about as special as Union or Tuple. And type-checking of Callable types is entirely structural, since that's what "callable" means fundamentally. (In fact it's a lot more fundamental than "int" or "str".)
I think what you're trying to do here is the following.
You have an API that takes a callback that implements some policy (e.g. for caching). There may be some predefined policies you are also fine with users implementing their own policies. However, you want them to explicitly state when they are writing a policy, so that only functions explicitly marked as policy functions are accepted as policy parameters in your APIs.
In a dynamic world, you could easily do this by requiring a
@policydecorator that just adds a special_policy_attribute to the function object, and in the API you check the policy callbacks for that attribute. But you wish that you could let mypy do this for you -- users would still add a@policydecorator, but that decorator would subtly modify the type, and your policy API arguments would be declared as only accepting such a policy. Then mypy would catch mistakes without runtime overhead.I think this is an interesting if slightly esoteric use case, and it looks possible that the solution onto which we are converging for simpler type aliases will made to work for callables too. For example:
gvanrossum commentedon Mar 20, 2016
@wittekm: I have a few questions about the details of your proposal.
(I also edited your example a bit to correct obvious typos, like a missing
selffor thenameproperty and to make theget()call correspond to the definition ofget_by_user_id(). I hope that's OK.)You are proposing asymmetric rules: any function that takes an int will silently accept a UserId, while any function that takes a UserId won't take an int. (In mypy -- at runtime they both accept either type since UserId is just int at runtime.)
A slight problem with this is that it seems to allow int operations on UserId instances, since operations are just syntactic sugar for functions. So this code would still be allowed:
I'm not sure if this is a show-stopping deficiency or something that we can live with. It seems to go against Jukka's (1) from #1284 (comment), where he says that the alias wouldn't inherit most operations. I'm curious if we can define a rule that actually follows (1). Certainly some operations should still be allowed, e.g.
__str__or__eq__. How would we define the set of operations thatTypeAlias()strips without special-casing e.g. int? This is essentially the problem from #1284 (comment) (the example with Celsius and Fahrenheit). We really need to decide about this before we can move forward.My other question is about using
cast()vs# typecomments. Your example hasand you wrote (both in the comment and in RULE 2) that it might also possibly use a cast. I would really hope we won't need to use or encourage casts here, since it's an expensive runtime operation (at least it is until we teach CPython about it -- right now it is a user-defined function, which is incredibly slow compared to a plain assignment). Unfortunately (and here @JukkaL might know better) I think that there's not much difference between the above and
since in both cases mypy sees an int (
user_id_as_int) in a context where a UserId is required. To understand this it helps to realize that putting a# typecomment on an assignment is really a crutch for a variable declaration -- in some hypothetical future Python syntax the above example might be written asIOW the
# type commentjust gives the type of the variable, not of the expression, and mypy then has to decide whether the expression's type is acceptable for the variable's type. There really can be only a single rule to decide whether an expression's type is acceptable for a given context (either a variable or argument supplies a context).However, all that seems to point us in the direction of requiring a cast, and as I said I really don't like that. How can we formulate a rule that does what we want? You tried with RULE 1 and RULE 2 but I think we'll need some additional subtlety that eludes me. Maybe we need to distinguish between different kinds of contexts (e.g. assignment being different from arguments) and allow conversion of int to UserId in one context but not another.
Going back to @JukkaL's list of use cases (1)-(4) and the Celsius/Fahrenheit example, I would also like to discuss runtime costs more. Some use cases require defining a new class using standard Python class syntax (e.g.
class Celsius(int): ...). There's no way that can have zero runtime cost, and I think if we're going that road it's not worth also adding special handling to mypy. The thing that's exciting would be to have aTypeAlias()special form that disappears entirely at runtime. But then I want to avoid the need for casts too. Here I am getting a headache. :-)Finally. Once we agree on how to do it I think we should come up with a better name, since "type alias" in PEP 484 is already used for pure aliases that aren't type-checked -- they are just shorthands to avoid having to write the same thing over and over.
Sorry for the rambling!
PS. Re: capitalization of UserID vs. UserId, I find the latter looks better. PEP 8 has something to say about abbreviations like HTTP (it prefers HTTPServerError), but I think that's really more about initialisms like HTTP (== HyperText Transfer Protocol), while to me ID feels more like a shortening of "Identity". I could see HTTP being evolved from H.T.T.P., but writing I.D. for Identity makes no sense -- even though I've seen it.
PAStheLoD commentedon Mar 20, 2016
@gvanrossum: Yes, thank you, that's the broadest use case I had in mind, sans the decorator, as I was envisioning that marking a function as this "non-equal alias" type would be enough information (both for the developer and mypy).
gvanrossum commentedon Mar 20, 2016
JukkaL commentedon Mar 20, 2016
@gvanrossum: You are correct, mypy uses the same rules for assignment and functions calls. Making assignments special wouldn't be too hard technically, but this special casing feels pretty ad-hoc to me. Users might expect that an assignment with a type annotation can be used as a cast in general and would get confused as it would only work for certain types. Using a type alias would still have a runtime cost (an extra assignment statement), and I'm not yet convinced that the runtime overhead of a call is significant enough to make the type system less consistent.
Instead of using
castor an assignment, there are other options we could pursue. Here are a few ideas.A) Make the alias a callable object
This should be slightly more efficient than a cast as there is only a single argument. This is arguably more readable and less error-prone that with a cast:
B) Special type that allows both
UserIdandintWe'd add a new kind of type, but we wouldn't need to touch the semantics of assignments. The idea is that a special type would accept
intas a value, but the type would still be a subtype ofUserId. This is similar to the unchecked union proposed by @ddfisher, but I'd rather use a different name for this. I don't have a great proposal for the name, though. The code would be efficient as we don't need an assignment or a call.Example:
The type
Implicit[UserID]would allowUserIdorint, but maybe it wouldn't allow other aliased types such asFileId(even if that is internally anint). It would be compatible withUserId-- it's basically a bridge betweenintandUserId.Implicit[t]probably isn't the best way to phrase this. Some other ideas, none of which seem very intuitive:UserId.implicitUnchecked[UserId]Promote[UserId]10 remaining items
gvanrossum commentedon Apr 7, 2016
I don't even know if this PEP change will go through, or when (even though I have it in the 3.5.2 milestone for the PEP -- that just means I want to think about it).
[-]Type aliases without implicit conversion ("newtype")[/-][+]Type aliases without implicit conversion ("Newtype")[/+]gvanrossum commentedon Jun 6, 2016
So the PEP work on this feature (spec in pep-0484.txt and runtime support in typing.py) is complete (python/typing#226) -- all we need is the mypy implementation now!
wittekm commentedon Jun 6, 2016
Awesome!
[-]Type aliases without implicit conversion ("Newtype")[/-][+]Type aliases without implicit conversion ("NewType")[/+]roganov commentedon Jul 20, 2016
Since
typing.py's implementation ofNewTypereturns new function every time, areT1andT2going to be different types?JukkaL commentedon Jul 20, 2016
The example would be rejected since the name passed to
NewTypedoesn't match the assignment target. However, this would define two different types:Add NewType (#1939)