Stop connecting to a server after n unsuccessful attempts
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")