Adding of a new keyword: when

I hereby propose the keyword when

This keyword has appeared to me as a good feature to simplify the process of creating and handling events in python. It would be much easier for newcomers to create a program with event using this keyword.

The when keyword create a thread that observe the variables in the condition. If it changes, it will check the condition again. If the condition is fulfilled, it will execute the indented code below.

Here’s an example of how it could be used:

# Add a code block here, if required
from time import sleep

i = 0

when i == 3:
   print("i is now equal to 3")

print("Before i change")
sleep(2)
i = 3
print("After i changed")
Before i change
i is now equal to 3
After i changed
3 Likes

Looks really interesting

1 Like

You did not explain how this is supposed to work. Presumably “when” creates a thread, but what does this thread do exactly?

1 Like

The thread created by the when keyword observe the variable in the condition. If it changes, it will check the condition again. If the condition is fulfilled, it will execute the indented code below.

Edit: i will add this explanation to the original post.

What should happen in

i=0

when i == 3:
    i = 2

when i == 3:
    i = 5

i = 3
print(f'{i=}')

Sometimes printing 2, sometimes 5?

5 Likes

What about

i = 0

when i == 3:
    i = 2
    i = 3

i = 3

Infinite loop? Or do when constructs only trigger sometimes (and if so, what are the rules?)

Is there an actual real-world use case for this idea? It’s far too easy to construct abstract examples that either show how the idea “might work”, or show potential problems with it. In order to usefully discuss this, we need:

  1. Practical examples of real-world cases where this would be useful (ideally ones that can’t easily be addressed using existing features).
  2. Examples of other languages with similar constructs, that will give us insights into how to design the feature.
9 Likes

I’d like to see this implemented as a function decorator.

@when(lambda: i == 3)
def three():
    print("i is now equal to 3")

This would make it clear what the proposal is and what the semantics are. Personally, I suspect it won’t get a lot of support, but the only way to know is to be completely sure what we’re even discussing.

3 Likes

I would see it work just like useEffect in React. It observe a state and start a function if the state change. Obviously there would side effects if the developer doesn’t pay attention to it but that’s expected of a multi threaded program. So yeah, sometimes 5, sometimes 2. This feature should be used with caution.

That depends on a very specific definition of “state”. React is not a general-purpose programming language. If you want something like that, you can certainly implement it, but instead of monitoring a variable, it would be its own thing, which would perform whatever actions you like when it is modified.

According to me, there is 2 possibilities for the example you provided. Either the variable in the when are local variables (a proposition of a friend of mine that I think goes against some ways python works), or it would be expected behavior like an infinite loop that the developer should be cautious about.

About the second part of your message, it’s basically a easy way of implementing threading in python. It would be useful for newcomers and erase boilerplate. For similar design in other language, I haven’t found one yet that would use this simple of a syntax.

Can you show some examples of how common this is?

I’m not sure of what you mean by “it would be its own thing”. Could you elaborate on it?

In React, when you ask for useState or useEffect or anything like that, they respond ONLY to React’s state management system, not to local variables. As such, these phenomena are completely separate from language level variables. If you want that sort of thing in Python, it’s not too hard to create; here’s a simple example:

class Value:
    def __init__(self, initial=None, name="unnamed"):
        self._value = initial
        self.name = name
    @property
    def value(self): return self._value
    @value.setter
    def value(self, newval):
        self._value = newval
        print("Setting", self.name, "to", newval)

Now, instead of simply assigning to a variable, you assign to somevar.value and it’ll trigger that code. You could add a generic set of hooks to this so you can attach your own code to it, or have a single class with many properties of this nature, or whatever you need. But it won’t monitor local variables.

4 Likes

Thanks for elaborating.

I may have not expressed what I thought correctly. The keyword when would not work exactly like useEffect in React, its just the way it would monitor the variable before checking. I understand a local variable in Python does not work like a state in React but I think it would be possible to simply add monitoring to variables that are in the when condition.

Also, I understand that its not too complex to implement a way to do it but a newcomer would not be able to understand how to do it or even know that its possible and that’s why I proposed this syntax.

The very first thing you need to do in order to implement this is to define the semantics clearly. At the moment, you’ve not done that.

The when keyword create a thread that observe the variables in the condition. If it changes, it will check the condition again. If the condition is fulfilled, it will execute the indented code below.

What do you mean by “variable” here? If the when construct is within a function, can it access local variables of the function? What about variables declared nonlocal or global? Can you observe an instance attribute (obj.something)? What about an element of a collection (some_list[2])? What about properties (attributes that are computed)?

Are conditions other than equality allowed? What about != or <? What about more complex checks like x % 2 == 1?

The definition says “when the variable changes” - there’s no mechanism in Python to trigger an action when a variable changes - do you plan on adding one, and if so how will that interact with optimisations like dead code elimination, and how do you plan on it not imposing a performance cost on the vast majority of variable modifications that don’t need to trigger an action?

Or if you’re just planning on a background thread periodically checking the value, how often does the thread check the monitored value? Can the user change this? What if it’s too fast (or too slow) for the application’s requirements? Can the user stop the monitoring? Or pause it? What if the variable changes from the trigger value to another value and back again in between two checks? Will that value change simply be missed? How will users be able to rely on a mechanism that might simply not work some times?

Does the monitoring thread persist after the function that contains it? Consider:

global_val = 0

def monitor_val(start_val):
    x = start_val
    when global_val == 19:
        x += 1
        print(x)

monitor_val(12)
global_val = 19 # The print needs to access the local variable x, which no longer exists...
global_val = 1
monitor_val(4)
global_val = 19
# Will *both* instances of the when block, from the two calls to monitor_val, be triggered?
# Will it therefore print 14 and 5? Just one? Which one?

How does the thread have access to x?

You have to answer all of these questions - language features can’t be specified in terms of things that “the developer should be cautious about”. It’s fine to have clearly and precisely defined semantics that result in undesirable behaviour like infinite loops[1], but not to have semantics that say such things “might” happen, without being clear how or why.

Personally, I think this proposal is potentially interesting as a possible (3rd party) library, offering a different way to do “background tasks”, but it’s currently far too under-specified. And it stands no chance in its current form of being accepted as a language feature. Quite apart from the unclear specification, which itself would be enough to kill it, threads in Python are currently implemented by library code, not by language constructs. I can’t see this being the mechanism that gets chosen as the fundamental (i.e., language-level) interface to threads.


  1. Although undefined results like “it might be 2 or 5” are usually not acceptable. ↩︎

4 Likes

In Tcl with trace add variable you can register a command that will be executed when the specified variable is accessed in the specified way (read, write, unset or array operations). But Tcl’s model of data and variables is very different from Python’s. You need a different language to make it working literally like you wrote it, you can’t just make few local changes in the parser and interpreter.

5 Likes

It sounds as though you’re looking for an implementation of the observer pattern in Python. While observing module-level variables might be awkward, writing code to observe attributes on instances of a class is definitely feasible.

For example, given suitable definitions of ICanHazObservableAttributes and Int, you can do this:

from time import sleep


class ClassWithI(ICanHazObservableAttributes):
    #: i is an observable attribute on instances of this class.
    i = Int(0)


# Create an observer that reacts to changes in i. In this case, it simply
# prints a message when i becomes 3.
def handle_i_change(event):
    if event.new == 3:
        print("i is now equal to 3")


# Create an instance, and register the observer for that instance.
obj = ClassWithI()
obj.observe(handle_i_change, "i")


# Now changing i to 3 issues the message.
print("Before i change")
sleep(2)
obj.i = 3
print("After i changed")

with output:

Before i change
i is now equal to 3
After i changed

Incidentally, the observers could be invoked synchronously - no need for extra threads (just like there are no extra threads involved when using useEffect in React). Or they could be scheduled as tasks on the asyncio task queue. Or they could, if you really wanted them to, be run in new threads, but extra threads aren’t necessary here.

There are existing libraries that already implement this sort of thing: traits, traitlets, param and atom are examples. (Spoiler: the above is actual working code, minus a first line that looks like: from traits.api import HasTraits as ICanHazObservableAttributes, Int.)

8 Likes

Tkinter has a Variable class with subclasses for string, integer, double, and boolean values. .trace_xyz methods define, add, and remove callback functions that are invoked which the Variable instance is read, written, or unset. The best doc I know of is the tkinter.py source.

This idea is interesting. I was thinking of a __when__ attribute : for example:

class A:
    def __init__(self, a, b) -> None:
        self.a = a
        self.b = b
    def __when__(self):
        return self.a > self.b
    

when A(1, 2):
    print('a > b')

that could be nice

Isn’t that the same as defining __bool__ and using if?