Can someone help me understand why the author of the python library chose to raise an exception on verification failures as opposed to returning false?
1. mainly: the C library has no concept of “wrong password”; only “verification failed with an error”. If you want to know why it failed, you need the error. As you can see in the example, a wrong password is "Decoding failed” which can also be your fault. It seems like they want to interpret their own failures as little possible. Therefore raising an exception with the error seemed the best way forward.
2. secondarily: in security context, I tend to prefer loud failures for dangerous problems so they don’t pass unnoticed by accident. ymmv
It isn’t raised in the middle of testing the hash. The testing is completely done in the Argon2 C library and the bindings raise an error if it returns an error. The Python library doesn’t do anything smart at all except calling C functions on strings.
Comparisons after hashing are naturally resistant to timing attacks, because you are not in direct control of the bytes being compared.
Just ask Bitcoin miners how hard it is to pick an input which results in a hash with a desired n-bit prefix.
But as a belt-and-suspenders you often see an attempt at fixed time comparisons of digests in any case.
Coincidentally, hashing before comparing can be used in scripting languages where the compare function will often be optimized out from under you, making constant time compare difficult or impossible to actually guarantee.
This principle doesn't necessarily mean functions should never return booleans, though.
Booleans are used in a variety of (popular) Python libraries when checking whether a password is correct (e.g. Django's `check_password` returns False if the password is wrong).
This is most likely the reason. It allows your code to follow the happy path for logging in, and treat verification as the exceptional case. Definitely a design choice by the author who self-identifies as a Pythonista.
It's not a big deal for this code, but in general this is good practice because
1. It makes it very obvious to the next developer which line is the one that is expected to raise that exception
2. One of the other lines could unintentionally raise that exception and mistakenly trigger the except clause. (This is more of an issue with Python's built in exceptions than with something very specific like this `VerificationError` example.)