Allow `__debug__` to be set at runtime

The builtins.__debug__ attribute controls whether assertions are checked at runtime.
Running Python with -O or -OO sets builtins.__debug__ to False, otherwise it is set to True.
Currently it cannot be changed at runtime.

builtins.__debug__ = False
  File "<python-input-3>", line 1
    builtins.__debug__ = False
    ^^^^^^^^^^^^^^^^^^
SyntaxError: cannot assign to __debug__

Should we allow it to be set at runtime, so that assertions can be turned on and off at runtime?
The performance impact would be negligible.

Is this a good idea? What do you think?

2 Likes

IMO there is no valid use case for it. We are either debugging or we are not. I can’t think of any case where we would toggle between debugging and not while the code is running.

3 Likes

When not debugging, are assertions stripped out of bytecode when the module is loaded, or are they still there and just get ignored?

If they are stripped out, I think toggling on __debug__ would require reloading the world–or it would only affect code that was interpreted in the future, and imported modules wouldn’t be affected. The latter option doesn’t sound so useful for debugging, and the former option seems like you might as well just restart in debug mode.

Currently they are stripped out, but I think @markshannon is suggesting to change that. The performance impact of that can be mitigated through runtime optimisations.

The real question is whether people might consider their assertions to be secrets, and therefore consider it a feature that they are not in the bytecode.

5 Likes

We can generate two versions of bytecode and switch between them on fly after changing __debug__, but I do not think this is worth a hassle.

2 Likes

We could allow it to be set to False at runtime, but setting it to True wouldn’t work because the bytecode might not include debugging information (the source code could be missing), and the necessary debugging libraries might not be available on the system (in a production environment).

That said, this change would introduce backward incompatibility.

I personally use -O and -OO in production.

My gut take is that even allowing builtins.__debug__ to be set to False at runtime is dangerous as we need to assume that there is code with logically load bearing calls in assert statements. Disabling at the global scope across all modules would hurt that.

Setting __debug__ no further than the current file’s globals() for use within the file seems more reasonable to support - though I don’t have a compelling reason why we’d want to.

We do not today (assignment to __debug__ even if specified as a global or nonlocal is always a SyntaxError - raised during parsing, not at runtime). The name __debug__ is treated as… very special.

6 Likes

I even thought about proposing to make __debug__ a true keyword, like True, False and None. This would make it less special and the compiler code slightly simpler.

2 Likes

Concur. It would just muddy the distinction between the modes. asserts should only be used for things the developer knows are true. Because we’re imperfect and will shoot ourselves in the foot if given the opportunity.

1 Like

I think there could be a valid use case, namely running tests within pytest. Say, you have code that uses if not __debug__: ... in a function, and you want to make sure to test whether that function still works with optimizations enabled. Currently, you would need to run pytest twice for this.

You would need to run the code twice anyway. The safest way to know whether it will work both with and without -O is to run it both with and without -O. Fortunately, we have tools that let us run Python twice with different parameters.

Sure, but if we could set __debug__ we could do it within the same pytest session. Which tools do you use to automate this?

I don’t see how this is a valid use case. Code that runs with -O will run without it just fine so long as people are only using asserts for things that are intended for catching violations of encoded invariants rather than production testing. That is, things they expect to be unconditionally true. If there are issues with misuse of assertions, people should find those with lining rules that look for dubious patterns like trying to catch an assertion

  1. Sometimes asserts are themselves correct, but influence the execution logic in such a way that subsequent statements depend on whether the assert was executed or not, thus leading to different behavior with or without -O.

    E.g.,

    d={"foo": "bar"}
    assert d.pop("foo") == "bar"
    print(len(d)) # value depends on whether this was run with or without -O
    

    This type of bug is very hard (if not impossible) to detect with linting. This isn’t some theoretical issue, we have seen this happen in our code (see e.g. SVM: make sure to pop memcpy kwargs even with -O by matthiasdiener · Pull Request #603 · inducer/pyopencl · GitHub).

  2. Even more importantly, this would also allow testing code that is guarded by if (not) __debug__:, whether the code in that if block itself is correct, or whether it causes other side effects like in item 1) above. Again, this is impossible to detect with linting.

If the code is intended to be run with and without -O then this code is crazily broken and shouldn’t exist. It’d be much, much better to remove the terrible code, rather than change how Python works.

4 Likes

Sure, the code I mentioned above is broken. The question is how to detect this particular kind of failure. Currently, the only way is to run the interpreter twice. Allowing to modify __debug__ at runtime would make this easier.

I’m aware that this will likely never be done, and maybe for good reasons. My point is that I think there are some valid use cases for this, imho.

But it would also provide a whole new way to write broken code…

No doubt about it.

Running the interpreter twice is also the only way to guarantee that your code executes from the same starting state.

This seems pretty easy to catch via linting or process to me. The only things that should exist as part of an assert are checking the value, truthiness, or type of an object that you have a strong direct reference to against either another value, type, or literal, and only when expected that the check should unconditionally pass and that there’s a development failure if they are violated. If that’s too hard, ban the use of asserts in your codebase entirely and write formal tests for each invariant.