Stop connecting to a server after n unsuccessful attempts I am trying to connect to a server.  
If the connection goes through, then print an apt message.  
If the connection does not go through, try for a total of n times. And quit after the nth attempt with an apt message.


I set up a mock server like this - 

class Server:
    def __init__(self):
        self.connection_status = random.choice([True, False, False, False])

    def get_connection(self):
        if self.connection_status:
            return "Connected"
        else:
            raise EnvironmentError("Connection Error")


And then I tested it as follows - 

def connect_to_server():
    s = Server()
    try:
        return s.get_connection()
    except EnvironmentError as e:
        return str(e)

def main():
    count = 0
    while count &lt; 3:
        connection = connect_to_server()
        if connection == "Connection Error":
            count += 1
            print(connection, count)
            if count == 3:
                print("No more connection attempts")
        else:
            count = 0
            print(connection)


So, after 3 re-connection attempts, the program will terminate. 

Is there a better / elegant way to do this?
The smart and polite thing to do when retrying is to wait some random (and possibly increasing) interval between retries.  The last thing you want to do to a failing server or network is to overwhelm it with connections as fast as the clients are able to generate them.

In connect_to_server(), squashing the EnvironmentError into a string is a bad idea.  Just letting the exception propagate would better convey the fact that a failure occurred.  Besides, what if the server really wanted to send the string "Connection Error" as its message?  Changing the client's behaviour based on the payload might even be considered a mild denial-of-service vulnerability.

The idiomatic way to write a counting loop in Python is to use range().

Defining a retry() function would be worthwhile for three reasons:


It makes your code self-documenting
It declutters your main()
You might be able to reuse the retry logic if it's needed elsewhere


One solution is to define a @retry decorator for functions:

from functools import wraps
from random import random
from time import sleep

def retry(max_tries=3, max_retry_interval=2, exceptions=[Exception],
          failure_callback=lambda exc, attempt, max_tries: None):
    """Tries calling the decorated function up to max_tries times until
    it succeeds, and returns its result.

    Any exceptions of the specified types thrown by the decorated function
    will trigger a retry.  The failure_callback is called with each failure,
    with the exception, the attempt number (starting with 0), and max_tries
    max_tries as callback parameters.

    There is a random delay of up to max_retry_interval seconds between
    tries.

    If max_tries attempts all result in failure, then the last encountered
    exception is raised. """
    def decorator(f):
        @wraps(f)
        def retry_wrapper(*args, **kwds):
            exception = None
            for attempt in range(max_tries):
                try:
                    return f(*args, **kwds)
                except Exception as e:
                    if not next((e for cls in exceptions if isinstance(e, cls)), None):
                        # Unexpected type of exception
                        raise 
                    failure_callback(e, attempt, max_tries)
                    exception = e
                    if attempt &lt; max_tries - 1:
                        sleep(max_retry_interval * random())
            if exception is not None:
                raise exception
        return retry_wrapper
    return decorator


Then your code would look like:

@retry(3, 5, [EnvironmentError], lambda exc, attempt, max_tries: print("Connection Error"))
def connect_to_server():
    …

def main():
    try:
        connection = connect_to_server()
        print(connection)
    except EnvironmentError as e:
        print(e)
        print("No more connection attempts")


Or, without hard-coding the retry parameters in a decorator:

def connect_to_server():
    …

def main():
    try:
        retry3times = retry(3, 5, [EnvironmentError],
                            lambda _, _, _: print("Connection Error"))
        connection = (retry3times(connect_to_server))()
        print(connection)
    except EnvironmentError as e:
        print(e)
        print("No more connection attempts")