Add a new token to force the methods to returns the previous object

You’re focusing too much on the specific example, which isn’t relevant to the proposal at hand. The point is, there are plenty of mutator methods that don’t return None; if you’re ignoring the return value, it doesn’t matter what it is. You could discard three elements off a list by calling stuff.pop() three times, but unless they JUST HAPPEN to be None, you wouldn’t be able to chain those calls together.

IMO any proposal along these lines should be agnostic as to the return value. I’m +0.1 on any such proposal in general, and they have a lot of problems to solve before they can be viable, but adding requirements like this isn’t making it any more useful.

3 Likes

Great! I agree with you :+1:

This makes me certain that Python shouldn’t impose these rules on the proposed feature.

Reinforcing the message above:

1 Like

Okay, cool. I thought that might have been your stance based on the conclusion, but there was too much time spent on specifics that I felt the need to clarify :slight_smile:

Use _, then.

import binascii

class CRC32:
    def __init__(self,value):
        if value:
            self.value = binascii.crc32(value)
        else:
            self.value = 0

    def update(self, data: bytes):
        self.value = binascii.crc32(data, self.value)


    @property
    def digest(self):
        return self.value & 0xffffffff

    @property
    def hexdigest(self):
        return f"{self.digest:08x}"


cs = (_ := CRC32(b'foo')).update(b'bar') or _.digest
print(cs)

would I actually write this? No. I’d:

cs = CRC32(b'foo')
cs.update(b'bar')
print(cs.digest)

and ignore anyone who complained about a scratch variable that’s on three consecutive lines.

5 Likes

:= + or actually works, but the danger is ending up reciting a spell and summoning some demon :smiling_face_with_tear:

The use of the token proposed in this discussion would be much more readable and intuitive.


You’re back to square one. :grin:


In this specific case of the CRC (a class I control), I was able to resolve it by improving the API with argument packing (specifically because __init__ does the same thing as update()), but the situation changes when you don’t have control and you are forced to create custom utilities, wrappers, or subclasses.

1 Like

I don’t think I’ve ever seen an &. operator in any language before - it could almost be a cursed hybrid of C++'s pass-by-reference (&) and attribute access. Or a confused misspelling of *. to dereference the method and access an attribute (by a C newbie)?

So maybe it’s just me, but this doesn’t feel very intuitive.

Nor does it feel that readable - especially when you’re trying to figure out where the value comes from, searching through the line of code, figuring out whether it was returned by a function, or comes from a stray & somewhere.

Not against the idea in general, but I think you’d need a lot more evidence to say that it’s “readable and intuitive”

1 Like

Look, it’s much easier to just explain that & is an operator that returns the evaluated object before the getter than to explain how (_ := CRC32(b'foo')).update(b'bar') or _.digest works.


To me, it seems clear that it’s returning the evaluated object before obtaining the attribute.

>>> @dataclass
... class Point:
...    x: int
...    y: int
....
>>> point = Point(1, 2)
>>> point
Point(x=1, y=2)
>>> point.x
1
>>> point.y
2
>>> point.&x
Point(x=1, y=2)
>>> point.&y
Point(x=1, y=2)

But I would like to reinforce what I said in the considerations:

Another alternative syntax that might be more intuitive because it resembles parent directory (..) as in paths:

>>> sha1()
<sha1 _hashlib.HASH object at ...>
>>>
>>> sha1(b'foo').update(b'bar') # None
>>>
>>> sha1(b'foo').update(b'bar')..
<sha1 _hashlib.HASH object at ...>
>>>
>>> sha1(b'foo').update(b'bar')..hexdigest()
'8843d7f92416211de9ebb963ff4ce28125932878'
>>>
>>> sha1(b'foo')..
  File "<python-input-1>", line 1
    sha1(b'foo')..
                 ^
SyntaxError: invalid syntax
>>> q = Queue().put(1)..put(2)..put(3)..
>>> x = q.get()..get()..get()

I personally thought it made the code look uglier.

Why not simply having a

class CRC():
    ...
    def __add__(self, somebytes):
        self.update(somebytes):)
        return self

Then

(CRC(b'foo') + b'bar').digest()

Because it’s common sense that __add__ should return something new instead of changing the object’s current state.

Even if it were a copy of the class, it wouldn’t be as efficient as argument packing.

Dart has the cascade notation which allows for easy access to an object without the need for each method to return self.

2 Likes

Wow, that’s great! This is an even more comprehensive version than the one proposed here, allowing for assignments as well. :open_mouth:

No, builder pattern was created because of lack of keyword arguments (**kwargs) in languages like Java. Like most patterns, it indicates language doesn’t support the thing speaker want or need.

To be clear, I think that proposing grammatical change just to bypass std API design decision isn’t constructive.

4 Likes

Don’t see this feature as a reason for people to neglect good API choices; see its use as a signal that you should rethink your API, and if there’s no other way, it’s okay to use it as a last resort.

I left the advantages listed in the gist above, but I’ll mention them here:

  1. Suggesting improvements for APIs often takes a long time to be accepted, and even then there’s a chance they’ll be rejected, making the process even more time-consuming;
  2. You are no longer required to create temporary variables in order to avoid losing the reference;
  3. Avoid creating custom utilities or subclasses to work around the problem.
  4. It can serve as a metric to indicate a poor API;
  5. You are not dependent on API maintainers because it works with any method; ~ @jsbueno
  6. It is moderately readable and intuitive, much more so than solutions with := + or, and much more concise.
  7. Since the API remains unchanged, it does not break compatibility with previous versions; ~ @jsbueno
  8. More incentive for creating methods that modify only the class state, so they can focus solely on functionality; ~ @jsbueno

In this case, I believe the Python interpreter could compare the data types and the memory address. This way, it can determine if it’s a kind of copy and issue a warning, similar to what happens when you forget to use await in a coroutine.

Something like:

>>> # both are types
>>> are_types = type(x) == type and type(y) == type
>>> 
>>> # are not types AND same type AND different memory address
>>> if not are_types and type(x) == type(y) and x is not y:
>>>     warnings.warn('Another instance belonging to the same type was lost when using the cascade operator (.&), this may have been a mistake.')
bool are_types = PyType_Check(x) && PyType_Check(y);

if (!are_types && Py_TYPE(x) == Py_TYPE(y) && !Py_Is(x, y)){
    PyExc_WarnEx(
        PyExc_RuntimeWarning,
        "Another instance belonging to the same type was lost when using the cascade operator (.&), this may have been a mistake",
        1
    );
}

This follows a standard object-oriented pattern used in Python and many other languages: calling a class creates an instance, on which methods can then be called.

What’s the benefit of doing this all in one line? :slightly_smiling_face:

One-liners are fine for simple calls, but splitting steps improves clarity, debugging, and future changes:

crc = CRC16(b'foo')
crc.update(b'bar')
crc.update(b'baz')
result = crc.digest()
1 Like

There’s no doubt about that, but you shouldn’t be forced to put the instance in a variable and in many cases you could simply do: CRC16(b'foo').digest().

This topic addresses cases where you are required to create a temporary variable to avoid losing the reference prior to the method call, which in many cases only makes your code too verbose. This will serve as syntactic sugar for the same result, but in a more practical, elegant, and concise way.


Are you suggesting that the one-liner is better than the current pattern enforced by the API choices?

result = CRC16(b'foo').&update(b'bar').&update(b'baz').digest()
crc = CRC16(b'foo')
crc.update(b'bar')
crc.update(b'baz')
result = crc.digest()

How would you read the one-liner? Something like, ’ result is equal to the digest of a CRC16 object initialized with b'foo', after updating it first with b'bar' and then with b'baz'.'? Of course not. What about the .& operator?

How should it be read?

result = CRC16(b'foo').&update(b'bar').&update(b'baz').digest()

More precisely, this topic is about adding a language feature to allow users to override the design decisions of library authors regarding the design of their library[1]. That’s not inherently a bad thing, but in general I’d regard “using a library in a way that wasn’t intended” as a code smell.

There are already plenty of ways of modifying the API of a library, including writing wrapper code and monkeypatching the library.

Given that questions of whether code is “verbose”, or “clear”, or “elegant” are inherently subjective, it seems very unlikely to me that this thread is going to come up with a sufficiently compelling argument for a language change unless someone comes up with a benefit of the new syntax that isn’t about allowing users to override the design of a library API - and so far, I’ve seen no such examples.


  1. If the user wrote the library code in question, they can simply change the called code, and this feature isn’t needed. ↩︎

3 Likes

I view it a bit differently. There is (at least some) desire in the ecosystem to support method chaining but so far the common practice is usually to not return self if a call has side effects. Brett made a great comment about that in another thread: Support method chaining in pathlib - #2 by brettcannon

Adding a dedicated operator would allow the API design to continue as is while simultaneously giving user the option to use chaining explicitly. This could ultimately lead to better APIs as devs might not feel pressured anymore to support chaining even if it doesn’t make sense for their project / API.

Not sure I’m convinced yet .& is the right syntax. Why not use .. for it?

>>> hex = (
>>>     sha1(b'foo')
>>>         ..update(b'bar')
>>>         ..hexdigest()
>>> )

The formatting is probably something that should be considered here as well. Using newlines like above does make sense IMO but can get verbose fairly quickly.

Another issue to consider might be async function calls. Not sure how to handle these best.

2 Likes