
On Sat, Jun 22, 2019 at 12:14:02PM -0400, James Lu wrote:
If a function that tends to return a context manager returns None, that should not mean an error occurred.
I suppose that *technically* this is true. It might be designed to return a context manager or None. But: 1. Because of the way Python returns None from functions when you fall out the end without an explicit return, "returns None is a programming error (a bug)" is the safe way to bet. 2. If it is intentional, and documented, the usual practice is to explicitly check for the exceptional value: mo = re.match(a, b) if mo: print(mo.group()) We don't give None a .group() method that returns None, so that lazy coders can write this and suppress the exception: assert hasattr(None, "group") print(re.match(a, b).group()) # doesn't raise any more, yay! because there are too many ways that None *is* an error and we don't want to cover them up. Getting an exception if you have an unexpected None is a *good thing*. So I don't think that with blocks should just automatically skip running None. For every context manager that intentionally returns None, there are probably a hundred that do it accidentally, where it is a bug. I can see at least three ways to deal with the very few "context manager or None" cases, that require no changes to the language: 1. Have the context manager explicitly return None and require the caller to explicitly check for it: x = Context(arg) if x is not None: with x: ... This adds two lines and one extra level of indentation, which is not too bad for something self-documenting and explicit. In Python 3.8 we can reduce it to one line and one extra level of indentation: if (x := Context(arg)) is not None: with x: ... which might be the best solution of all. 2. Have the context manager raise an exception (not an error!) and require the caller to explicitly catch it: try: with Context(arg) as x: ... except ExceptionalCase: pass This adds three lines and one extra level of indentation. On the other hand, if your context manager can raise on actual errors as well, this is the most natural way to solve the problem: try: with Context(arg) as x: ... except SomethingBadError as err: handle(err) except ExceptionalCase: pass 3. Write the context manager to return a do-nothing mock-up instead of None. You might be able to use the Mock object in the standard library for this (I have never used Mock, so I don't know if it is suitable) but worst case it adds a one-off cost to the writer of the context manager, but it gives the caller do-nothing handling for free: with ContextOrMock(arg) as x: ... If x happens to be a do-nothing mock, the code in the block will do nothing.
If an error or unexpected condition occurred, an exception should be thrown. Errors and exceptions should result in the code within the with statement not executing.
Isn't that how the with statement already works?
We could add a new constant to Python, “Dont.” It’s a global falsey Singleton and a context manager that causes the code inside “with” not to execute.
Don't what? I might be more sympathetic to that if the name was more descriptive. Say, "SkipWithBlock", and we make it an exception. To skip the with-block, have the context manager raise SkipWithBlock from inside its __enter__ method. This is less disruptive because: - it won't hide the exception from buggy context managers that accidentally return None; - it requires an explicit raise inside the context manager; - although it adds a new built-in, it is an exception, and psychologically people tend to think of exceptions as seperate from the builtins (at least *I* do, and people I've spoken to). All this supposes that there is a moderately common need for a context manager to skip the with block, and that the existing solutions aren't sufficient. -- Steven