Does finally ensure some code gets run atomically, no matter what?
Asked Answered
F

2

6

Assume I'm going to write a Python script that catches the KeyboardInterrupt exception to be able to get terminated by the user using Ctrl+C safely

However, I can't put all critical actions (like file writes) into the catch block because it relies on local variables and to make sure a subsequent Ctrl+C does not break it anyway.

Would it work and be good practice to use a try-catch block with empty (pass) try part and all the code inside the finally part to define this snippet as "atomic, interrupt-safe code" which may not get interrupted mid-way?

Example:

try:
    with open("file.txt", "w") as f:
        for i in range(1000000):
            # imagine something useful that takes very long instead
            data = str(data ** (data ** data))
            try:
                pass
            finally:
                # ensure that this code is not interrupted to prevent file corruption:
                f.write(data)

except KeyboardInterrupt:
        print("User aborted, data created so far saved in file.txt")
        exit(0)

In this example I don't care for the currently produced data string, i.e. that creation could be interrupted and no write would be triggered. But once the write started, it must be completed, that's all I want to ensure. Also, what would happen if an exception (or KeyboardInterrupt) happened while performing the write inside the finally clause?

Foppish answered 21/4, 2016 at 6:15 Comment(1)
finally does not mean "atomic, interrupt-safe code". All sorts of things can stop a finally or prevent it from running, including interrupts. It's just a control flow statement.Isabea
O
5

Code in finally can still be interrupted too. Python makes no guarantees about this; all it guarantees is that execution will switch to the finally suite after the try suite completed or if an exception in the try suite was raised. A try can only handle exceptions raised within its scope, not outside of it, and finally is outside of that scope.

As such there is no point in using try on a pass statement. The pass is a no-op, it won't ever be interrupted, but the finally suite can easily be interrupted still.

You'll need to pick a different technique. You could write to a separate file and move that into place on successful completion; the OS guarantees that a file move is atomic, for example. Or record your last successful write position, and truncate the file to that point if a next write is interrupted. Or write markers in your file that signal a successful record, so that reads know what to ignore.

Oleary answered 21/4, 2016 at 6:24 Comment(9)
Yes, sorry. This was just a hypothetical example and I just noticed that it would not have been useful this way myself. I updated the example.Foppish
@ByteCommander: same comments apply. The noop won't be interrupted, your finally suite is not going to be executed.Oleary
@ByteCommander: you also seem to think that data will receive a value as str(..) produces data. That's not the case. A Python expression has to run to completion before assignment can take place. If the str(..) call is interrupted, data will never be assigned to.Oleary
Yes, of course. I said that I don't care about the current data snippet, I just want to make sure the file write action gets completed at once without being interrupted. If an interrupt happens before a write starts, it's okay to discard the not yet saved data produced so far.Foppish
@ByteCommander: right, that wasn't clear in the first iteration of your question. I've updated your title to ask what you are really asking.Oleary
Thanks for the edit, now we're talking about the same thing. :)Foppish
@MartijnPieters Would you mind explaining the following? "The noop won't be interrupted, your finally suite is not going to be executed." My understanding is that the finally block is executed regardless of what happens in the try block (exception or no). And a try: pass \nfinally: print("finally") seems to contradict you.Nicolina
@Dunes: yes, it is executed, but there is no point in using it on pass. The only reason to use finally is to ensure stuff runs after other stuff, regardless of why the other stuff exited. pass will never exit.Oleary
@Dunes: I didn't say finally wouldn't get executed. I'm saying the try...finally is redundant.Oleary
F
5

In your case, there is no problem, because file writes are atomic, but if you have some file object implementetion, that is more complex, your try-except is in the wrong place. You have to place exception handling around the write:

try:
    f.write(data)
except:
    #do some action to restore file integrity
    raise

For example, if you write binary data, you could to the following:

filepos = f.tell()
try:
    f.write(data)
except:
    # remove the already written data
    f.seek(filepos)
    f.truncate()
    raise
Flores answered 21/4, 2016 at 6:34 Comment(4)
So one file write is atomic, but multiple subsequent file writes would not be, which means I would have to group them somehow to ensure they're all made. Is there no way to ensure a code block gets properly finished once it got started? Do I only have the option to roll back partial, incomplete actions?Foppish
@Daniel: yes, but I was thrown by the title. The edit made things a little clearer.Oleary
"file writes are atomic" - that's a misleading statement. Sufficiently up-to-date versions of Python 2.7 or Python 3.x will keep writing if interrupted by a signal, but anyone stuck on an older version can have their file.write calls interrupted. There are also other problems that could interfere with a file write, like someone jostling a USB stick at the wrong time.Isabea
@user2357112: as I stated, "... in your case .. "; there are many other failures, that are outside the scope of pythons exception handling.Flores

© 2022 - 2024 — McMap. All rights reserved.