A common task in programming is accessing some resource that might or might not be available. If the resource is available, processing can happen as usual — but the resource should be safely released when it is no longer needed; however, if the resource is not available when requested, usually some exception is raised and should be handled or propagated. Exceptions can also occur during the processing of an open resource — in which case the resource should be closed.
This task is common enough that some languages like Java have implemented try-with-resources constructs that allow for this pattern. A resource can be accessed — but if any exceptions occur while acquiring the resource or during processing of the resource, they can be caught.
Python currently does not have a single construct to achieve this — resources can be acquired and closed with context managers, and exceptions can be caught with try statements. Combining these constructs, while a common enough task, is clunky.
The pattern of resource acquisition/processing with error handling is common enough that I feel Python should allow with statements to include except, else, and finally blocks to handle any errors that are raised during a context manager’s lifetime.
For example:
with open(file, "rb") as f:
config = json.load(f)
except FileNotFoundError as e:
panic_with_message(f"Cannot find file: `{file}`", e)
except JSONDecodeError as e:
panic_with_message(f"Invalid JSON in: `{file}`", e)
else:
run(config=config)
finally:
global_cache.clear()
Context managers do have the ability to handle exceptions as they are being exited (For example, as Suppress does); however, there is currently no elegant way of providing custom handling logic to context managers.
This has been discussed before: `try with` Syntactic Sugar - #8 by oscarbenjamin — but the conversation there seemed to revolve mostly around aesthetics and code length rather than providing a codified implementation of this common resource handling pattern.
The example block of code is quite appealing. But doesn’t this need to decide (or at least it should be documented) whether the context manager’s __exit__ method is called first, or the finally: block is executed first? A clue is in the name, but a combination of try: and with makes that unambiguous and explicit.
I’m in favour of the former — I feel the __exit__ code should run before any exceptions or other blocks are handled — which would close any resources as soon as the with block exits. This is also how Java’s try with resources behaves:
public class Main {
public static void main(String[] args) {
try (Closing closing = new Closing()) {
System.out.println("In try block");
throw new Exception();
} catch (Exception e) {
System.out.println("Catching Exception");
} finally {
System.out.println("Finally");
}
}
}
class Closing implements AutoCloseable {
public Closing() {
System.out.println("Creating Closing");
}
@Override
public void close() {
System.out.println("Closing now");
}
}
prints:
Creating Closing
In try block
Closing now
Catching Exception
Finally
__exit__ running before any except blocks also removes confusion around the order of events for exceptions that happen in __enter__ or the context manager constructor (e.g. FileNotFoundErrors) which are not followed by __exit__ and exceptions that happen in the with block which are followed by __exit__
You are conflating two different sources of errors: with calling open and with using the object that a successful call to open returns. They can and should be separated. The with statement is already a sort of try statement; any exceptions are passed to the __exit__ method of the context manager. In this case, you want to handle an exception that occurs before the file-like object even exists.
What you’ve shown should be written like this
try:
f = open(file, "rb")
except FileNotFoundError:
panic_with_message(f"Cannot find file: `{file}`", e)
else:
# *If* the file was opened successfully, ensure it is closed
# no matter what happens while we are using it
with f:
try:
config = json.load(f)
except JSONDecodeError as e:
panic_with_message(f"Invalid JSON in: `{file}`", e)
finally:
global_cache.clear()
It’s only 2 lines longer, and it’s more clear about when a file actually needs to be closed. It’s not important that the call to open appear at the start of the with statement, only that the value it can produce does.
Elegance is in the eye of the beholder. I find it elegant that with blocks and try blocks are orthogonal components that can be easily composed. I would find it less elegant to combine the two.
If the only benefit is saving 9 characters ("try:\n ") and a level of indentation, it seems like a clear loss to me.