Making `as` an operator

as only works in Python for aliasing import names, and I feel like there’s some wasted potential there. Perhaps it could become an operator? Note that I’m not very familiar with how Python’s grammar works, so if turning the as keyword into an operator as well would be a completely breaking change, I would be happy to hear why.

The main use case for a feature like this would be syntactic sugar for (typing) casting, similar to how TypeScript’s as works:

from typing import Any

("hello" as Any).foo  # the type checker would be fine with this, but would not actually "cast" it at runtime

I do realize we have typing.Cast for this kind of situation, but it wouldn’t be the first time new syntax was implemented solely for typing.

This feature could come with an __as__ protocol, which could also be used to make some cool things. For example, say you had some JSON from a REST API and wanted to convert it to a corresponding dataclass, it could implement __as__ to do that:

@dataclass
class Person:
    first: str
    favorite_color: str

    @classmethod
    def __as__(cls, other: object) -> Person:
        if isinstance(other, dict):
            other["favorite_color"] = other.pop("favoriteColor")  # rename the key
            return cls(**other)
        return super().__as__(other)

json = requests.get("...").json()  # lets pretend this returns {"first": "Peter", "favoriteColor": "blue"}
print((json as Person).first)  # Peter

Again, this would mostly be syntactic sugar (that could perhaps increase readability), and I would be happy to hear any reasons on why this wouldn’t work or why it’s a bad idea.

1 Like

Related previous discussions:

Just to note, as is also used in the with and except statements.

as was rejected as an option for assignment expressions

stuff = [[f(x) as y, x/y] for x in range(5)]

in part because of semantic overloading of this one keyword.

4 Likes

I didn’t like the idea of assignment expressions in the first place, but this way doesn’t seem unreasonable to me. Existing uses of as are all fundamentally assignments.

What OP proposes would use as for typing - which is different, but still perfectly logical for the word as ordinarily understood. I don’t think it’s worse than how, for example, in refers to a membership test in ordinary expressions but sets up iteration in a for loop or comprehension.

On the other hand, I think that the described __as__ protocol is a step too far. It seems like the intended purpose is really to give syntactic sugar for typing purposes - presumably, it would allow the compiler to remove useless-at-runtime casts rather than leaving in do-nothing calls to typing.cast. So it seems completely wrong that the same operation might sometimes invoke a dunder that actually evaluates code and creates a new object. The described sort of creation of a Person from a dictionary, is IMO much better done with a named classmethod: Person.from_dict(json).

2 Likes

Yes, but not PURELY assignments. Consider that, at the time of Python 3.8 (when PEP 572 was accepted), a with statement did not allow parentheses (that restriction was relaxed in Python 3.10). So if assignment expressions had been EXPR as NAME instead of NAME := EXPR, there would be an extremely subtle distinction here:

with (ctx() as f):
with ctx() as f:

Using it for typing is, IMO, even more dangerously confusing. Does it actually change anything, or does it just make a declarative change that type checkers will use? The __as__ protocol suggests the former, but otherwise, people will assume that a type check declaration won’t actually do conversions.

1 Like

I was thinking it would probably do what it does in TypeScript, where it just signals to the type checker that something got casted. The __as__ protocol was mainly just thinking out loud, it would probably make more sense to just compile the as out.

This made me think, what if an as operator was used not for type hints, but for actual type conversion? The distinguishing feature between it and using regular constructors would be that conversion behaviour would be delegated to the object being converted, rather than to the targeted type.

For example to convert an object to list you of course simply use list(my_object). But if you were to define an class which should convert to a list in a different way than the list constructor behaves, you would define an appropriate __as__ for your class, then call my_object as list. This could replace the current practice of simply defining specific methods for such conversions, such as numpy.ndarray.tolist.

I do realise that this is a pretty clear violation of the “Preferably only one obvious way to do something” principal by providing two different ways to convert objects, though I do think that there are definite use cases for it, so I thought it was worth mentioning.

list constructor uses the iterable interface to define how to convert your object to a list.
Then you can act on this conversion by defining a custom __iter__ method on your type.

And that’s how other standard types work (__iter__ for list/tuple/set/dict or keys+__getitem__ for dict, __str__ for str, __int__ for int, etc.).
You can define your own interface/protocol if you need one for your types.

I only pointed that out because I imagine the same arguments against as in that discussion would apply here as well.

But my main issue with this proposal is that it implies some sort of “preferred” status to a particular conversion because it’s supported by syntax, not just an API call. Consider the ndarray example. The NumPy documentation itself points out the difference between list(a) and a.to_list() (assuming a = np.uint32([1,2])):

  1. list(a) produces a value of type list[np.unint32].
  2. a.to_list() produces a value of type list[int], adding a type conversion to each element of the array in the process of converting the array itself to a list.

Hypothetically, there could be additional to_list-style methods:

  1. a.to_list_float128
  2. a.to_list_float64
  3. a.to_list_int64
  4. etc

Now we’ve got 5+ different ways to convert an ndarray to a list: which one “deserves” to be the implementation of a as list? (I’m not going to consider a as list[np.int64] et al. as options, as this would be the first suggestion that non-runtime types should have any semantic meaning at runtime.) If anything, I would argue a as list should be equivalent to list(a) as the most “basic” conversion (iteration with nothing else), but some other conversions may be less straightforward.

I’m more for using this as a shorthand for typing.Cast (throwing out the __as__ entirely and just compiling it out), but going on your point with the __as__ protocol, I would argue that something like a as list[np.int64] would be totally fine, as it’s up to NumPy to define that behavior, not Python. Plenty of external libraries give type hints meaning at runtime for all sorts of reasons.

If this idea gets thrown out, I think speeding up cast should be something that could be looked into, as that was a complaint in other threads with this topic. Perhaps cast could be implemented in C with METH_FASTCALL?

This is what I mean though, since list uses the iterable interface in order to be general, it means that there is no obvious way to delegate the conversion to the object itself, in cases where the iterable interface is undesirable. The only option is to create a new method specifically for this purpose. Other types would also have this problem, any object that wants to convert to another type without using the defined interface cannot do so without defining new methods specifically for this.

I would see list(x) as the general case, any iterable object will work, while x as list is the specialized case when there is a better conversion method known by x but not list.

I am personally strongly against adding syntax with no associated runtime semantics. PEP 695 for example is actually really cool and with just a bit of looking into the inner workings you can get quite cool runtime semantics. Completely compiling out as would make it no better than an inline comment, and we can already use comments to type cast if we want to, without having to overload a keyword in a distracting and incompatible way.

And if it does have runtime semantics, I doubt there will be much performance benefit over cast. It will still need to do a lookup in some object or another, call a function, potentially defined in python and potentially create new objects. (even ignoring the ambiguity if it should be a method of the left or right hand operand, or both).

2 Likes

Hypothetically, there could be additional to_list -style methods

This is a good point, but doesnt this problem also exist for regular constructors? If I define a type, I might have 4 or 5 different interfaces for my constructor in mind with different behaviors/advantages, but ultimately I must choose one to be the “default” constructor. Some variants if similar enough might be included using keyword arguments to the constructor, etc. but otherwise my additional constructor methods must be defined separately. And I think a lot of the time there really is one preferred (or default) conversion method.

Very strong dislike here of the syntax.

What’s being proposed here makes it really confusing with all existing uses of as which is for aliasing/limited form of inline assignments. I generally would’ve preferred as syntax instead of walrus operator for inline assignments, but that ship already sailed.

Using this syntax in context managers or except handler, where this syntax could mingle with the existing as syntax used there would lead to confusion.

Fair point, but Python does mix up keyword uses from time to time though, for example in being used in both for loops and as a membership operator.