Error "index out of range" when working with strings in a for loop in python

Question:

I’m very new to python and I’m practicing different exercises.

I need to write a program to decode a string. The original string has been modified by adding, after each vowel (letters ’a’, ’e’, ’i’, ’o’ and ’u’), the letter ’p’ and then that same vowel again.
For example, the word “kemija” becomes “kepemipijapa” and the word “paprika” becomes “papapripikapa”.

 vowel = ['a', 'e', 'i', 'o', 'u']
 input_word = list(input())

 for i in range(len(input_word)):
     if input_word[i] in vowel:
         input_word.pop(i + 1)
         input_word.pop(i + 2)

 print(input_word)

The algorithm I had in mind was to detect the index for which the item is a vowel and then remove the following 2 items after this item ,so if input_word[0] == 'e' then the next 2 items (input_word[1], input_word[2]) must be removed from the list. For the sample input zepelepenapa, I get this error message : IndexError: pop index out of range even when I change the for loop to range(len(input_word) - 2) ,again I get this same error.

thanks in advance

Asked By: Pantea

||

Answers:

pop() removes an item at the given position in the list and returns it. This alters the list in place.

For example if I have:

my_list = [1,2,3,4]
n = my_list.pop()

will return n = 4 in this instance. If I was to print my_list after this operation it would return [1,2,3]. So the length of the list will change every time pop() is used. That is why you are getting IndexError: pop index out of range.

So to solve this we should avoid using pop() since it’s really not needed in this situation. The following will work:

word = 'kemija'
vowels = ['a', 'e', 'i', 'o', 'u']
    
new_word = []
    
for w in word:
    if w in vowels:
        new_word.extend([w,'p',w])
        # alternatively you could use .append() over .extend() but would need more lines: 
        # new_word.append(w)
        # new_word.append('p')
        # new_word.append(w)
    else:
        new_word.append(w)
    
decoded_word = ''.join(new_word)
print(decoded_word)
Answered By: ayliah

The loop will run a number of times equal to the original length of input_word, due to range(len(input_word)). An IndexError will occur if input_word is shortened inside the loop, because the code inside the loop tries to access every element in the original list input_word with the expression input_word[i] (and, for some values of input_word, the if block could even attempt to pop items off the list beyond its original length, due to the (i + 1) and (i + 2)).

Hardcoding the loop definition with a specific number like 2, e.g. with range(len(input_word) - 2), to make it run fewer times to account for removed letters isn’t a general solution, because the number of letters to be removed is initially unknown (it could be 0, 2, 4, …).

Here are a couple of possible solutions:

  1. Instead of removing items from input_word, create a new list output_word and add letters to it if they meet the criteria. Use a helper list skip_these_indices to keep track of indices that should be "removed" from input_word so they can be skipped when building up the new list output_word:
vowel = ['a', 'e', 'i', 'o', 'u']
input_word = list("zepelepenapa")
output_word = []
skip_these_indices = []

for i in range(len(input_word)):

    # if letter 'i' shouldn't be skipped, add it to output_word
    if i not in skip_these_indices:
        output_word.append(input_word[i])

        # check whether to skip the next two letters after 'i'
        if input_word[i] in vowel:
            skip_these_indices.append(i + 1)
            skip_these_indices.append(i + 2)

print(skip_these_indices)       # [2, 3, 6, 7, 10, 11]
print(output_word)              # ['z', 'e', 'l', 'e', 'n', 'a']
print(''.join(output_word))     # zelena
  1. Alternatively, use two loops. The first loop will keep track of which letters should be removed in a list called remove_these_indices. The second loop will remove them from input_word:
vowel = ['a', 'e', 'i', 'o', 'u']
input_word = list("zepelepenapa")
remove_these_indices = []

# loop 1 -- find letters to remove
for i in range(len(input_word)):

    # if letter 'i' isn't already marked for removal,
    # check whether we should remove the next two letters
    if i not in remove_these_indices:
        if input_word[i] in vowel:
            remove_these_indices.append(i + 1)
            remove_these_indices.append(i + 2)

# loop 2 -- remove the letters (pop in reverse to avoid IndexError)
for i in reversed(remove_these_indices):

    # if input_word has a vowel in the last two positions,
    # without a "p" and the same vowel after it,
    # which it shouldn't based on the algorithm you
    # described for generating the coded word,
    # this 'if' statement will avoid popping
    # elements that don't exist
    if i < len(input_word):
        input_word.pop(i)

print(remove_these_indices) # [2, 3, 6, 7, 10, 11]
print(input_word)           # ['z', 'e', 'l', 'e', 'n', 'a']
print(''.join(input_word))  # zelena
Answered By: Patrick Cantwell