“Better” is subjective. You don’t use ; or := because it’s pretty, but because it’s useful in specific situations (if it weren’t, it wouldn’t have been added), so you can’t generalize; you have to judge when you write.
I think perhaps the question should have been whether I find it more concise in small cases, and the answer is yes.
Of course, nothing prevents you from exaggerating, except your own common sense, and this is also true for the use of any other resource.
result = (
CRC16(b'foo')
.&update(b'bar')
.&update(b'baz')
.digest()
)
If you find the example above useful and a more direct alternative, perhaps you could consider using it.
I don’t read any code as ‘variable X is equal to Y’, I read it as ‘the result of expression Y is assigned to variable X’, then I read the expression step by step, and this makes any reading efficient and understandable.
Steps:
The CRC16 class is initialized with b'foo'.
Its update() method is invoked with b'bar' but without losing the reference.
Its update() method is invoked with b'baz' but without losing the reference.
Finally its digest() method is invoked and returns the checksum result in bytes.
The return value is stored in the variable result.
And of course, we can’t forget the most useful feature of the language: comments.
# Calculates the CRC-16 checksum and obtains your result in bytes.
result = (
# Initialize checksum with `b'foo'`.
CRC16(b'foo')
# Update checksum with `b'bar'` but without losing the reference.
.&update(b'bar')
# Update checksum with `b'baz'` but without losing the reference.
.&update(b'baz')
# Obtains the checksum result in bytes.
.digest()
)
I thought it could convey the idea of ’keep the same reference’. But other suggestions are welcome; we’re here to discuss them.
I still prefer to think of .& as a ‘Keep Reference’, but thinking of .. as ‘Go Back’ is also interesting. We can call this behavior ‘Explicit Side-Effect Composition’, but I don’t know what else we could call this token.
I think this framing gets closer to the core issue.
There is an observable tension in the ecosystem between semantic clarity (mutating methods returning None) and the desire for composability and chaining.
Today, that tension is often resolved at the API level, sometimes leading to methods returning self primarily for ergonomic reasons, even when they have significant side effects.
A dedicated operator allows API authors to preserve the existing semantic contract, while giving users an explicit, opt-in mechanism for chaining.
In that sense, the feature doesn’t override API design, but rather removes pressure from it, allowing clearer APIs rather than more fluent ones by default.
Another way to think about it is that you really DO have a temporary variable, but it isn’t named. In BIND (DNS server), you can omit the name to mean “same name again” - for example:
gideon IN A 10.1.2.3
gideon IN A 10.4.5.6
gideon IN AAAA ::1234
gideon IN AAAA ::5678
You could omit the name gideon from all but the first line, and it’ll have the same effect.
What you’re doing here would work similarly if strung out on multiple lines, as in the commented example from @d3cryptofc above.
Unfortunately, this also reveals a fundamental weakness of the proposal: that you can already do exactly this. Replace your .& token with _. and start with _=, and you have something that already works just fine.
I agree that conceptually there is an intermediate object, but I don’t think this is equivalent to using an unnamed temporary variable like _.
Using _ still introduces a name into the surrounding scope, relies on convention rather than syntax, and is not local to the expression itself.
It also conflicts with existing uses of _ (REPL value, i18n, pattern matching), and does not compose naturally.
Given the direction the discussion is taking, the intent of the proposal is not to eliminate temporaries, but to make the discarding of a return value explicit and local at the call site, without affecting scope or API semantics.
I’m okay with the idea in principle, but I don’t much like the proposed syntax. It seems like an arbitrary use of random punctuation. The idea that “&” means “keep the same reference” is a very long stretch, requiring the reader to be familiar with a different language and make a rather contrived analogy with it.
I’m trying to think if there are any precedents in other languages. In Smalltalk there’s a way of sending multiple messages to the same receiver, if I remember rightly it’s something like
receiver message1; message2; message3
A translation of this into Python might look like
obj.method1(); .method2(); .method3()
I don’t think this conflicts with existing use of semicolons, because an expression can’t currently start with a dot.
Another possibility, to convey the idea of a “sequence of method calls”, might be
Thank you to everyone who is taking the time to discuss this topic.
I would like to inform you that the proposal has been revised. The initial thesis is no longer focused on subjective aspects such as code aesthetics, and now focuses on more objective technical issues.
This would not work at all because if obj.method1() returns an object without a reference to the original object, there’s no good way for the subsequent operations to retrieve it.
This is how I would write it if I were reading it as a task:
crc = CRC16(b'foo')
crc.update(b'bar')
crc.update(b'baz')
result = crc.digest()
That’s the obvious way to write it, in the sense of the Zen of Python. The proposed syntax introduces an alternative, but it requires additional explanation to justify its use…
The update method is simply a special case that returns nothing (None). How would one infer that a method returns a value (including None)? You cannot just return the object itself.
The proposal does not attempt to infer whether a method returns a meaningful
value.
The behavior is explicit at the call site: when used the new syntax it means that the return value of that specific call is intentionally discarded, regardless of what it is.
No assumptions are made about method semantics or return types.
OK, so to summarise what I believe is being proposed:
obj.&something computes obj.something, but then discards the value of the expression and instead returns obj. So obj.&something is (in effect) equivalent to (obj.something, obj)[1].
Here, something is typically a method call, but could be a property or attribute (although that would probably be useless in practice).
Some observations:
Multiple spellings of the operation have been suggested instead of .&. None seem particularly obvious, and the fact that there’s no obvious spelling of the operator (or indeed, term that describes the operation) suggests that it’s not particularly intuitive what’s going on. New operations can be learned, so this isn’t a showstopper, but it is a concern that should be addressed.
The operation can’t easily be replicated using a user-defined function - the method call is clumsy to pass to a function in an “unevaluated” form, and evaluating it loses the original object. So if we want this feature, new syntax is clearly the right approach to take.
There’s not a lot of “prior art” here. I think someone mentioned a similar construct in Swift, but that’s about it as far as I know. Most language ecosystems seem to focus on either a “fluent” style, or a more procedural style, rather than trying to support both - but it’s a decision made at the API level, not the language level.
The feature is at its core intended to allow the user to use an API in a way the API author didn’t intend. Even if that’s expressed as “giving the choice to the user”, it’s still not immediately obvious that this is something we should be supporting or encouraging.
The .& “operator” is a strange thing - it’s not like other operators in the sense that there’s no way of overriding the behaviour (i.e., no equivalent of __getattr__). That may be a problem for things like proxy objects.
There’s a fair bit to think about there. Personally, I see the arguments for the feature, but I still don’t think it’s useful enough to justify new syntax. That’s just a personal view, though. But if the proposal is to progress, I think the next step is probably to address the various questions that have been brought up (which I tried to capture here) in a reasonably objective and persuasive way. It’s clear that the OP is a fan of the idea, but they now need to look at this from the point of view of someone who isn’t a fan, and come up with a way of getting support from those people. Because otherwise, the proposal will probably end up getting abandoned due to lack of support[1].
My reading of the discussion here is that there’s a certain amount of interest in the technical aspects of the proposal, but not much enthusiasm for it… ↩︎
Although as I pointed out above, I did not think this would work because obj.foo() returns an object without a reference to obj so &. would have no way of retrieving obj, I now think this may technically work if a . b &. c is made a ternary operator. But the fact that this operator starts with a . means that it can cause a non-insignificant slowdown to parsing because of backtracking from the current grammar rule for the . operator.
Furthermore, to make a . b &. c &. d work, we would need a 4-operand operator, and so on and so forth. The possible backtracking it’ll require will be significant.
So in conclusion, the syntax has to be in form of a &. b &. c for it to be viable.
Note that although I support the core idea of giving users the choice to express multiple updates to an object in a single expression, I don’t see any of the syntaxes proposed so far as intuitively readable, nor can I come up with one. So I’m +0 on this.
I thought for some more and came up with what I think is a more intuitively readable and technically viable syntax of an action list enclosed in a pair of curly brackets:
The brackets make it clear that the enclosed attribute accesses are modifications to the outer object preceding the brackets before the object is passed along.
This contrasts with the proposed syntax and the various variations in this thread, where with Class() .& method1() .& method2() it can be unclear to those unfamiliar with the new operator that method2 is accessing the object before the very first .& in the series of .&s rather than the object immediately before the closest .&.
Are there any real-life use cases? I mean, any situation where chaining is more important than the actual return value of the method.
Note that the toy example with the update method might not reflect typical code review expectations. The library author could have returned the object itself if that made sense, but that’s not how update is intended to be used.
Well, It presuppose that do you knows what you doing - then you knows the API. Given this conditions I honestly don’t know if this propose will be at least considered.
Anyway, I’m leaving the discussion. My apologies to everyone here. Continue to discussion who want, I will be supporting this propose from afar. Thank you to everyone who participated.
It’s hypothetical (to my knowledge), but a potential example that springs to my mind is with database connections.
The Database API 2.0 specification in PEP249 does not define a required return value for Cursor.execute(...), but every implementation I’ve seen has it return self, specifically to allow chaining, e.g.
with (
example_db_library.connect(...) as conn,
conn.cursor() as cur
):
results = cur.execute(...).fetchall()
If there were a particular implementation that, for whatever reason, decided that their Cursor.execute method should return something else other than self (e.g. the value of Cursor.rowcount or Cursor.description), then that would be spec-compliant, but would be different than most other database driver implementations.
I don’t think that’s a great argument to add new syntax just to avoid an extra line, but if something like cur.execute(...).&fetchall() already existed in the language, I’d almost certainly do that over
cur.execute(...)
results = cur.fetchall()
I suppose I’d end up as a -0 on the overall proposal. I’d probably occasionally use it if it already existed, but it doesn’t seem useful enough to justify adding syntax.
Like the walrus operator, this is a quality-of-life improvement that, while yes, one can certainly live without, can make such common patterns of codes handier to write.