Can I keep an expression with side effects in the Watch window from being reevaluated?


I notice that if I have an item in the Watch window such as N = N + 1, then every time I continue the program and then break, this expression is re-evaluated.

When the debug script evaluates the expression the first time, is there a way it can tell the IDE that there were side effects (or possible side effects), so that the expression will not be evaluated again without some specific instruction from the user?


pminaev wrote Jan 10 at 7:13 PM

Right now, we basically treat all expressions in Watch as side-effect-free - there are no flags and such internally to discriminate, much less any way to set those flags.

Ideally, we'd want to figure out if something is side-effect-free or not more or less automatically. For example, this particular code obviously isn't, and we can tell that by parsing it and seeing that it has an assignment.

In fact, arguably, this particular code should not be valid in Watch at all, because assignment is a statement rather than expression. Currently, we just hand it over to exec as is without trying to validate it in any way, which makes it possible to use all kinds of weird stuff like assignments and loops there (and potentially break the debugger if you enter something like while True: pass as a watch). Statements don't really make sense for Watch anyway because they always return None; for cases where you want to run them just for their side effects, Immediate window and Debug Interactive are better options. So the immediate action item is that we should validate expressions in Watch, and reject those that cannot be parsed as expressions.

It still leaves a question of what to do with bona fide expressions that have side effects (e.g. mutating function calls). We can't reasonably detect those in a generic fashion, but we can make some assumptions and expose some options. We do in fact already have a set of options for the Debug Interactive that deal with the same issue - it uses eval to drive code completion, and so the issue of what to do with side effects came up there as well. We currently expose the following options in Tools -> Options -> Python Tools -> Debug Interactive Window:
  • Never evaluate expressions
  • Never evaluate expressions containing calls <-- default
  • Always evaluate expressions
We could add a new property page in Options with similar choices for Watch, with the same default setting (it seems reasonable to assume that simple attribute access and indexing won't mutate). Only instead of "never evaluate", it would read "explicitly evaluate". So e.g. if you have the "expressions containing calls" option selected, and add a watch like x.add(123), we would show that Watch item as grayed out, with a refresh icon where the value should be and text explaining that it must be explicitly evaluated to produce the results. Basically, copy the C# debugger behavior for this.

We could also add some knowledge of commonly used builtins that are side-effect-free, like len(), so that we are not unnecessarily pessimistic when determining whether the expression has side effects or not. This is a bit trickier though as we'd then need to figure out if that len() actually refers to a built-in len(), or to something overriding it in the current scope. We could still do it by evaling len itself and inspecting the object which it points to, and making a decision based on its __name__ and __module__ (or __objclass__ for methods), and store that list in a documented place such that it can be customized.

pminaev wrote Jan 15 at 10:15 PM