The Alternate Egos of Python's 'Else'

Jun 12, 2021

Python's else keyword has some neat functionalities that I haven't run into in other programming languages. As someone new to the language, you'll obviously know the if/else block, exemplified in this simplified selection of a preposition ( if you go to this article, you can find plenty of examples where this function is inadequate):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def select_preposition(word):
    ''' Selects a preposition for the given word

        :param word:  String which preposition will be inserted before
        :return:  Selected preposition (a or an)
        '''
    if word.lower()[0] in 'aeiou':
        return 'an'
    else:
        return 'a'

# Main function
if __name__ == '__main__':
    words = [
        'Man',
        'Animal',
        'House',
        'Wombat'
    ]

    for word in words:
        preposition = select_preposition(word)
        print(f'{preposition} {word.lower()}')

Of course, that gives you:

a man
an animal
a house
a wombat

But did you also know that the else keyword can be used with for loops?

Here's an example of a script which adds friends' contact information to a spreadsheet if an entry has not been found:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def get_contact_info(name):
    ''' Gets the contact information or adds it to the spreadsheet
        if not available

        :param name:  Name of contact to add
        :return:  Contact information
        '''
    # Initialize empty record for new contacts
    record = {
        'name': name,
        'phone': '',
        'email': ''
    }

    # Find contact in contact_info.csv. If one is found,
    # break from the loop early to avoid the else condition
    with open('contact_info.csv', 'a+') as contact_info:
        contact_info.seek(0)
        for line in contact_info:
            if name in line:
                record = {list(record.keys())[count]: val
                          for count, val in enumerate(line.split(','))}
                break

        # No contact found. Add new entry to spreadsheet
        else:
            contact_info.write(
                f'{record['name']},{record['phone']},{record['email']}\n')

        return record

# Main function
if __name__ == '__main__':
    for name in ['Janet', 'Chris', 'Bob']:
        contact_info = get_contact_info(name)
        print(f'Name:{contact_info["name"]}\n'
              f'Phone:{contact_info["phone"]}\n'
              f'Email:{contact_info["email"]}')

Within the with block, each line of the file is searched for the friend's name. If the name is found, a new record with the contact information is populated and the loop is exited with the break keyword. This avoids the functionality in the else block of the loop.

If the loop is able to complete without breaking, the else block is executed. In this case, that means that a record was not found for the friend that was requested. A new record is written to the CSV.

Here's the output of the program when Janet's and Chris' contact information is in the CSV and Bob must be added:

Name:Janet
Phone:555-1234
Email:[email protected]

Name:Chris
Phone:555-5555
Email:[email protected]

Name:Bob
Phone:
Email:

The contents of the file after execution looks like this:

Janet,555-1234,[email protected]
Chris,555-5555,[email protected]
Bob,,

No new records were created for Janet and Chris, since they were found in the file and the function broke early from the loop. A new record was created for Bob since the loop was allowed to complete.

The same functionality can be used for the while statement.

The else keyword's third hat is related to the try/ except block. It can be placed after except to execute any code which should only run if an exception is not encountered. Note that this is different than a finally block which runs after the try/except whether or not there was an exception encountered.

The next example of a file reader demonstrates the difference between the 'finally' and 'else' keywords in the context of a try/except block.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def read_file_contents(path):
    ''' Prints the file contents and number of characters read

        :param path:  Path to file
        :return: None
        '''
    # Attempt to open the file. If the file is not found, a
    # FileNotFoundError will be raised. 
    try:
        file_length = 0
        contents = ''

        print(('-' * 10) + path + ('-' * 10))
        file_handle = open(path, 'r')

    # Handle the FileNotFoundError by setting the file 'contents'
    # to the string below. Execution will then skip the 'else'
    # block since an exception was encountered.
    except FileNotFoundError:
        contents = f'File {path} not found'

    # The open completed without an exception. We can now read
    # from the file handle and calculate the length of the file.
    else:
        contents = file_handle.read()
        file_length = len(contents)

    # Regardless of whether an exception was raised, print the
    # number of bytes read and the file contents (or our fake
    # contents if nothing was read). 
    finally:
        print(f'Characters read: {file_length}\n')
        print(contents)

# Main function reads from a good file (contact_info.csv from last
# example), or a file which cannot be found. 
if __name__ == '__main__':
    read_file_contents('contact_info.csv')
    read_file_contents('not_a_file.txt')

This script results in the following:

----------contact_info.csv----------
Characters read: 82

Janet,555-1234,[email protected]
Chris,555-5555,[email protected]
Bob,,

----------not_a_file.txt----------
Characters read: 0

File not_a_file.txt not found

You can see that the except and the else block are exclusive, while the finally block will always be executed, even if an exception is raised. Note that if there are multiple except blocks (for multiple exception types), there is still only one else block. No need for an else after each exception type.

I find that this is really helpful in writing explicit code. Instead of having to guess where exceptions come from in a giant try block, the else keyword guides the readers of your code to what is expected to raise an exception, and what is just a dependency of the suspect code (and is not expected to raise an exception itself).

And isn't explicit code the beauty of Python?