# Do-While loop in Python, with limit on the amount of 'do's

I'm in a location with poor Internet access. In order to ensure that a call to an external service succeeds, I am wrapping the call in a pseudo-do-while in Python. Additionally, I am limiting the call to running a maximum of three times:

def post_safe(url, params):

done = 0
max_tries = 3
messages = []

while done<max_tries:
try:
response = requests.post(url, data=params)
except Exception as e:
messages.append(e)
time.sleep(1)
else:
done = max_tries+1
done += 1

if done==max_tries:
output = "%s\n" % (datetime.now().strftime('%Y-%m-%d %H:%M'),)
output+= "requests() failed 3 times:\n"
for m in messages:
output+= m+"\n"
print(output)

return False

return True


I personally am not satisfied with the 'Pythoness' of this code. It looks inelegant, and I would have preferred to use the type bool for done but that would require a second variable to count the iterations.

Is there a cleaner way to achieve a do-while in Python while limiting the amount of iterations?

-

The else branch in your try is hackish (skipping over the max_tries) so I added success variable.
(Although you asked not to avoid it in the question, I think it's the better way, and I'll explain why.)

It also nicely allowed me to

• replace if done==max_tries with if not success;
• remove else branch altogether;
• return success instead of having two return statements.

I think it reads more easily now.

def post_safe(url, params):
messages = []

success = False
max_retries = 3
retries = 0

while retries < max_retries and not success:
try:
response = requests.post(url, data=params)
success = True
except Exception as e:
messages.append(e)
time.sleep(1)

retries += 1

if not success:
output = "%s\n" % (datetime.now().strftime('%Y-%m-%d %H:%M'),)
output+= "requests() failed 3 times:\n"
for m in messages:
output+= m+"\n"
print(output)

return success


You can also get rid of success by returning True early in try:

def post_safe(url, params):
messages = []

retries = 0
max_retries = 3

while retries < max_retries:
try:
response = requests.post(url, data=params)
return True
except Exception as e:
messages.append(e)
time.sleep(1)

retries += 1

output = "%s\n" % (datetime.now().strftime('%Y-%m-%d %H:%M'),)
output+= "requests() failed 3 times:\n"
for m in messages:
output+= m+"\n"
print(output)

return False


I don't like this approach. It wins us a variable and two lines but obfuscates the flow:

• In which case will the last block run? When we had an if not success condition, it was obvious, but now from a quick glance it looks like this block executes every time;
• What are function's return points? The first example had just one, and it was obvious what the return value represented.

Let me quote PEP 20:

• Sparse is better than dense.

Finally, success may be an “extra” variable from computer's point of view, but for a human, it's the expression that determines if the last block of code runs. It's good to give it a name.

-
Thanks, Dan. Though just a tad more readable, I don't like the use of the extra variable (I mention that in the OP). I do think that the extra readability is due to the better-named variable (done was a terrible name). I'm hoping that a more 'Pythonic' answer comes along. I would almost expect this to be a common design pattern. –  dotancohen Jan 7 '14 at 16:37
@dotancohen I added a second example that doesn't need success, but personally I find it less obvious. –  Dan Jan 7 '14 at 16:42
There were some terrific answers here. I choose this answer based on the readability. It feels wasteful, but it is very readable and maintainable. Thank you! –  dotancohen Jan 8 '14 at 12:39

Python has a language feature just for that: else clauses on loops.

Loop statements may have an else clause; it is executed when the loop terminates through exhaustion of the list (with for) or when the condition becomes false (with while), but not when the loop is terminated by a break statement.

def post_safe(url, params):
max_tries = 3
messages = []

for _ in range(max_tries):
try:
response = requests.post(url, data=params)
return True
except Exception as e:
messages.append(e)
time.sleep(1)

else:
print(datetime.now().strftime('%Y-%m-%d %H:%M'))
print("requests() failed %d times:" % (max_tries))
for m in messages:
print(m)
return False


I'd also make the following changes:

• The else clause on the try block is unnecessary; it can be replaced with a break within the try block just after calling .post(). But in the end, you'll just end up returning True, so why not get out of there right away with return True? (For that matter, the else block that I just suggested is also unnecessary, as the else could be removed and its contents outdented by one level.)
• It seems easier to print each line separately than to build a multi-line string to print all at once. You might want to consider logging instead.
-
huh ... lsned - learn something new every day. –  rolfl Jan 7 '14 at 17:47
Considering the return True in the try block, the else clause on the for loop is unnecessary too! But I must say, for-else is great and thank you for mentioning it. –  dotancohen Jan 7 '14 at 18:36

One of the beauties of Python is that it can be made to read like English. With a very small amount of messing with your code, you can get to this, which I feel is much more readable:

def post_safe(url, params):

done = False
tries_left = 3
messages = []

while tries_left and not done:
tries_left -= 1
try:
response = requests.post(url, data=params)
done = True
except Exception as e:
messages.append(e)
time.sleep(1)

if not done:
output = "%s\n" % (datetime.now().strftime('%Y-%m-%d %H:%M'),)
output+= "requests() failed 3 times:\n"
for m in messages:
output+= m+"\n"
print(output)

return done

-
Great idea about decrementing the counter. –  Dan Jan 7 '14 at 17:08
That's what optimization freaks teach you to do in C: it seems that comparing to 0 is cheaper in some computer architectures... –  Jaime Jan 7 '14 at 17:09
Thank you, I also like the idea of decrementing the counter. I'll deliberate a bit before choosing one of the two excellent answers. Maybe a third will show up in the meantime! –  dotancohen Jan 7 '14 at 17:25
nit-pick - sometimes I like val += 1 instead of val++ because it makes sense in that context, but, because - and = look more alike, I never recommend val -= 1. just use val-- –  rolfl Jan 7 '14 at 17:44
@Jaime - I only open my mouth to swap feet. Appreciate that you didn't add 'idiot' to your comment ;-) –  rolfl Jan 7 '14 at 19:03