Lecture: Sequences and Loops

Today we'll talk about sequences, an abstract type that is exemplified by concrete types: such as str, list, and range.
We will talk about the common properties of sequences, and how they are used in enabling another way for looping execution.

Table of Contents

  1. Motivating example: counting vowels
  2. for loops
  3. Sequence: lists
  4. Iterating Over Lists
  5. Sequence: range
  6. Ignoring the Iteration Variable
  7. Looping over Indices of Sequences
  8. Operations with sequences
  9. Practice

1. Sequences

Sequences in Python are ordered collections of values. We have already seen one example of a sequence: a string. A string is an ordered collection of characters. There are others as well.

Sequences in Python:

  • Strings: an ordered collection of characters
  • Lists: an ordered collection of any type
  • Ranges: an ordered collection of integers

Many times we use sequences in conjunctions with loops. Sequences hold data and loops provide a way to look through the sequence one element at a time. We've already seen an example of that from last lecture with countVowels.

While Loops with Sequences

In the last lecture we saw that we could use a while loop to count the number of vowels in a string. How did we do that? We iterated over each letter by getting the index of each letter. The index allowed us to extract each element (i.e., letter) from the string sequence. We can use this approach with any sequence as we will see shortly.

In [1]:
def isVowel(letter):
    """a predicate that returns true for letters that are vowels.
    """
    return len(letter) == 1 and letter.lower() in "aeiou"
In [2]:
def countVowels(word):
    """an accumulation function that returns the number of vowels in a word.
    """
    vowelCount = 0
    i = 0
    
    while i < len(word):
        if isVowel(word[i]):
            vowelCount += 1
        i += 1
        
    return vowelCount
In [3]:
countVowels("perspicacious")
Out[3]:
6

Here is another example using a while loop to print out all the letters in a string. You can see we use the same paradigm by going through each index of the string to extract the letter and print it.

In [4]:
word = "Boston"
i = 0

while i < len(word):
    print(word[i])
    i += 1
B
o
s
t
o
n

While (pun intended) the above code works perfectly fine, there is actually a syntatically cleaner way to write the code above. If we use a for loop, we do not need to worry about using the indices to extract each element from the string.

2. for loops with strings

A for loop is also a structure to repeat code. Anything that you want to do with a for loop can be done with a while loop, but a for loop is generally syntatically cleaner and more concise. In Computer Science, we say that a for loop is syntactic sugar. Syntactic sugar are syntactical elements of a programming language that are redundant (i.e., can be done by some other paradigm) but is designed to make things easier to read or express. We can always use a while loop to handle our iteration needs but a for loop offers a more readable and concise option for certain problems.

Important: Any for loop can be written as a while loop, but not all while loops can be written as for loops. We will see why shortly.

The cell below shows the for loop version of the code above to print each letter from a string.

All for loops take two things:

  1. An iteration variable - here this is letter. An iteration variable is a new variable that is declared by the for loop. Each element of the sequence is set to letter.
  2. A sequence - here this is the string held in word.

The code that we repeat is the print statement. We repeat for as many elements as there are in the sequence. Notice how we do not need indices.

In [5]:
word = 'Boston'
for letter in word:
    print('letter has the value', letter)
letter has the value B
letter has the value o
letter has the value s
letter has the value t
letter has the value o
letter has the value n

For loop: countVowels

Below is the for loop approach to countVowels.

In [6]:
def countVowels(word):
    """an accumulation function that returns the number of vowels in a word.
    """
    vowelCount = 0      # accumulation variable
    for letter in word:
        if isVowel(letter):
            vowelCount += 1
    return vowelCount
In [7]:
countVowels("perspicacious")
Out[7]:
6

Your turn: Write countLetter

Finish the definition of the countLetter function below so that it returns the number of times the specified character char appears in the provided word word.

In [1]:
def countLetter(word, char):
    "Counts the number of times 'char' (a single letter) appears in 'word'."
    # Your code here
    charCount = 0
    for letter in word:
        if letter == char:
            charCount += 1
    return charCount
In [2]:
countLetter('banana', 'b') # should be 1
Out[2]:
1
In [3]:
countLetter('banana', 'a') # should be 3
Out[3]:
3

3. Sequence: list

for loops iterate over sequences. Strings are an example of a sequence. The for loop assigns each character of the string to the iteration variable. This section discusses a new type called lists which are also sequences.

Lists are another type in Python just like strings or ints. Lists are an ordered sequence of values. Lists are declared with square brackets and items are separated by commas. Lists are an example of a data structure. Lists are meant to store data where the order of the data matters.

In [11]:
[42, 3, -7]
Out[11]:
[42, 3, -7]

Lists can have the same type or different types of values.

In [12]:
[True, 3.4, "hello"]
Out[12]:
[True, 3.4, 'hello']

Similar to other types that we have seen: integer, float and string, there is a built-in function with the name of the type that generates values of that types by converting from a type to another. The function list below converts strings to a list of string characters.

In [13]:
list("Wendy Wellesley")
Out[13]:
['W', 'e', 'n', 'd', 'y', ' ', 'W', 'e', 'l', 'l', 'e', 's', 'l', 'e', 'y']

Question: Will it work the same way with integers?

In [14]:
list(100)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-f4a5da02c9e6> in <module>
----> 1 list(100)

TypeError: 'int' object is not iterable

Question: What if you convert the number into a string first, and then try to turn it into a list. Will that work?

In [15]:
list(str(3.14159265))
Out[15]:
['3', '.', '1', '4', '1', '5', '9', '2', '6', '5']

4. Iterating over Lists

We can use the same syntax for iterating over lists just as we did with strings. With lists, the iteration variable gets assigned to each element in the list.

In [16]:
books = ["Beloved", "All the light we cannot see", "The Song of Achilles", "Othello"]
In [17]:
for book in books:
    print("Book:", book)
Book: Beloved
Book: All the light we cannot see
Book: The Song of Achilles
Book: Othello

Your Turn: printVowelBooks

Below write a function called printVowelBooks that prints out all the books in a list of books that start with a vowel. printVowelBooks should take a single parameter that is a list of books.

In [18]:
# Your code here

def printVowelBooks(books):
    for book in books:
        if isVowel(book[0]):
            print(book)
In [19]:
printVowelBooks(books) # should print out two books from the list above
All the light we cannot see
Othello

5. Sequence: range

Strings and lists are sequences. Another type of sequence is a range. A range produces a sequence of ordered numbers. To see why a range might be useful, consider the example below:

In [20]:
#Let's check how many divisors a number has.
number = 8
divisors = 0
for candidate in [1,2,3,4,5,6,7,8]:
    if number % candidate == 0:
        divisors = divisors + 1
print(number, 'has', divisors, 'divisors')        
8 has 4 divisors
In [21]:
# And again...
number = 11
divisors = 0
for candidate in [1,2,3,4,5,6,7,8,9,10,11]:
    if number % candidate == 0:
        divisors = divisors + 1
print(number, 'has', divisors, 'divisors') 
11 has 2 divisors

Notice that we have to hardcode the list of numbers. This means we can't use this code on any number. We'll learn about a new tool that lets us generalize our divisor counting now.

Creating sequences of numbers with the built-in function range

The first argument of range starts the sequence, and all numbers up to (but not including) the second argument are generated.

In [22]:
range(0, 10)
Out[22]:
range(0, 10)
In [23]:
type(range(0, 10))
Out[23]:
range

The range function returns an object that holds the sequence. To see the contents of range's sequence, pass it into the built-in list function that will return a list of numbers. We will talk more about the list function in subsequent classes.

In [24]:
list(range(0, 10))
Out[24]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

We'll get an empty list when the two arguments are the same, or the first is greater than the second:

In [25]:
a = range(3, 3)
b = range(3, 1)
print(list(a))
print(list(b))
[]
[]

Omiting the first argument means we want to start at 0 by default

In [26]:
list(range(8))
Out[26]:
[0, 1, 2, 3, 4, 5, 6, 7]

Using range with three arguments

range takes a third argument, the value of the step between two generated values. If this value is missing, by default step is 1.

In [27]:
# The even numbers
list(range(0, 10, 2))
Out[27]:
[0, 2, 4, 6, 8]
In [28]:
# Stepping by 10
list(range(3, 70, 10))
Out[28]:
[3, 13, 23, 33, 43, 53, 63]

The step can be negative. When the step is negative, the start needs to be greater than the end of the range.

In [29]:
list(range(10, 0, -1))
Out[29]:
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Your turn: Write the range to produce all nonnegative integers up to and including 50 that are divisible by 5:

In [30]:
# Your code here:
nums = range(0,51,5)
list(nums)
Out[30]:
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

We can iterate over this sequence just like any other sequence!

In [31]:
for i in range(11):     # Starts at 0, goes up to n-1
    print(i)
0
1
2
3
4
5
6
7
8
9
10

If we want the range of numbers from 1 up to and including number, here's one way we can do that:

In [32]:
number = 11
for i in range(number):
    print(i + 1)                # Add 1 to the number before using it
1
2
3
4
5
6
7
8
9
10
11

Remember that we can also explicitly set both the start and end values for range:

In [33]:
number = 13
for i in range(1, number + 1):   # Ask range to give us the right numbers directly
    print(i)
1
2
3
4
5
6
7
8
9
10
11
12
13

Now we can generalize our divisor counting!

In [34]:
def countDivisors(number):
    """A function that returns the total of divisors for a given number.
    """
    divisors = 0
    for candidate in range(1, number + 1):
        if number % candidate == 0:
            divisors = divisors + 1
    return divisors
In [35]:
countDivisors(8)
Out[35]:
4
In [36]:
countDivisors(11)
Out[36]:
2

Your Turn: Write doublesUpTo

Write a function doublesUpTo which takes a number x and prints out each of the integers up to and including x doubled. For example, doublesUpTo(4) should print out

0
2
4
6
8
In [37]:
# Your code here
def doublesUpTo(x):
    for i in range(x+1):
        print(i * 2)
        
doublesUpTo(4)
0
2
4
6
8

6. Ignoring the Iteration Variable

You can also use range to repeat some code multiple times even if you don't use the iteration variable.

In [38]:
for _ in range(10):                                    # The variable name _ is just a normal variable name, 
    print('Say this tongue twister ten times fast!')   # but some programmers use it as a convention to indicate to
                                                       # a reader of the code that syntax requires a variable name 
                                                       # but they don't plan to use the value stored in that variable.
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!
Say this tongue twister ten times fast!

To prove that the underscore or using the underscore in a variable name is valid, consider the syntax below:

In [39]:
_ = 3
In [40]:
_
Out[40]:
3

Your Turn: Sum Four Integers

Write a function called sumIntegers that takes a number numOfInts specifying the number of integers to sum and prompts the user numOfInts times to provide an integer. The function should return the sum of all those integers. Here is a sample call below.

In [1]: sumIntegers(3)
Provide an integer: 4
Provide an integer: 2
Provide an integer: 1
Out [1]: 7
In [41]:
# Your code here

def sumIntegers(numOfIntegers):
    count = 0
    for _ in range(numOfIntegers):
        count += int(input("Provide an integer: "))
    return count
In [42]:
sumIntegers(3)
Provide an integer: 5
Provide an integer: 7
Provide an integer: 9
Out[42]:
21
In [44]:
sumIntegers(5)
Provide an integer: 1
Provide an integer: 3
Provide an integer: 5
Provide an integer: 7
Provide an integer: 9
Out[44]:
25

7. Index Loops vs. Value Loops

Up to this point, we have seen how for loops iterate over a sequence by setting the iteration variable to each value in the sequence. For a string, the iteration variable is set to each letter in the string. For a list, the iteration variable is set to each element in the list. For a range, the iteration variable is set to each number in the range. All of the for loops we have examined so far are meant to extract each value from the sequence. When we iterate directly over a sequence like this, we call it a "value loop".

Value Loop with Strings

In [45]:
sequence = "computer"
for value in sequence:
    print(value)
c
o
m
p
u
t
e
r

Value Loop with Lists

In [46]:
sequence = [True, None, 3]
for value in sequence:
    print(value)
True
None
3

Value Loop with Ranges

In [47]:
sequence = range(4)
for value in sequence:
    print(value)
0
1
2
3

Problem: How do we get the position of each value?

While the for loop patterns we have seen so far help us solve many problems, they do not encompass all the ways we can use loops. For example, how would I write a loop that prints out every other element in a list? We can, of course, use a while loops as we have seen before, but how could we do it with a for loop?

In [48]:
# How can I print out all the letters from this list
seq = ['a', 1, 'b', 2, 'c', 3]
for element in seq:
    print(element) # this prints out every element...   
a
1
b
2
c
3

The position of elements is important in solving this problem. The loop above only gives us the values in the sequence. We need more information! We need the positions of the values!

How can I get the position of the elements? Here we can use range and len to help us. The solution to this problem is to generate a new sequence whose elements are the indices of the original sequence. For example, given the list ['a', 1, 'b', 2, 'c', 3], we want to be able to generate a sequence that has the numbers 0, 1, 2, 3, 4, and 5.

In [49]:
indices = range(len(seq)) # a sequence of indices

list(indices) # list is only being used here to show you the elements in range
Out[49]:
[0, 1, 2, 3, 4, 5]

Now that we have a sequence of the indices, we will loop over the indices and set the iteration variable to each index in the sequence of indices. We call this an index loop because we set the iteration variable to each index in the original sequence as opposed to its value.

In [50]:
# this code prints out all the indices from the original sequence

for index in range(len(seq)):
    print(index)
0
1
2
3
4
5

The great thing about indices is that we can also use the index to get value at that index as well using the subscripting operator (i.e., []).

In [51]:
for index in range(len(seq)):
    print("Index:", index, "Value:", seq[index])
Index: 0 Value: a
Index: 1 Value: 1
Index: 2 Value: b
Index: 3 Value: 2
Index: 4 Value: c
Index: 5 Value: 3

Now that we know how to generate the indices from any sequence, we can solve our original problem. Note here that we use the iteration variable i. This is a common choice for index loops as i indicates an index. We can, of course, choose something else but this is a common convention.

In [52]:
# a loop that prints only the letters from the list ['a', 1, 'b', 2, 'c', 3]

for i in range(len(seq)):
    if i % 2 == 0: # we use the index to help us filter out the positions we care about
        print(seq[i])
a
b
c

To summarize, let's make index loops for each type of sequence that we know.

Index Loop with Strings

In [53]:
sequence = "computer"
for i in range(len(sequence)):
    print("Index:", i, "Value:", sequence[i])
Index: 0 Value: c
Index: 1 Value: o
Index: 2 Value: m
Index: 3 Value: p
Index: 4 Value: u
Index: 5 Value: t
Index: 6 Value: e
Index: 7 Value: r

Index Loop with Lists

In [54]:
sequence = [4.1, 3, 'a']
for i in range(len(sequence)):
    print("Index:", i, "Value:", sequence[i])
Index: 0 Value: 4.1
Index: 1 Value: 3
Index: 2 Value: a

Index Loop with Ranges

In [55]:
sequence = range(100, 108, 2)
for i in range(len(sequence)):
    print("Index:", i, "Value:", sequence[i])    
Index: 0 Value: 100
Index: 1 Value: 102
Index: 2 Value: 104
Index: 3 Value: 106

Another Example using Index Loops

Sometimes your loop needs both the values in a sequence as well as their indices. For example, let's imagine that you're running an amusement park. When people want to get on a ride, they put their initials on a waitlist. Then, you display the waitlist on a screen so folks can know how close to the front of the line they are. So for example, if some prominent American figures wanted to ride, the screen might show something like:

#0: BHO
#1: MLRO
#2: MAO
#3: SO
#4: AOC
#5: NPP
#6: RBG

Our first attempt might look like this:

In [56]:
waitlist = ['BHO', 'MLRO', 'MAO', 'SO', 'AOC', 'NPP', 'RBG']
for name in waitlist:
    print(name)
BHO
MLRO
MAO
SO
AOC
NPP
RBG

But this doesn't print the indices, which are an important piece of information that we wanted to display. In this case, we will often use range to generate indices, like so:

In [57]:
waitlist = ['BHO', 'MLRO', 'MAO', 'SO', 'AOC', 'NPP', 'RBG']
for spot in range(len(waitlist)):           # Range generates the indices
    name = waitlist[spot]                   # Use indexing to get the value
    print('#' + str(spot) + ': ' + name)    # Perform a calculation using both value and index.
#0: BHO
#1: MLRO
#2: MAO
#3: SO
#4: AOC
#5: NPP
#6: RBG
In [58]:
# Once Barack gets off the list and is on the rollercoaster, you can see that everyone else has moved up
waitlist = ['MLRO', 'MAO', 'SO', 'AOC', 'NPP', 'RBG']
for spot in range(len(waitlist)):
    name = waitlist[spot]
    print('#' + str(spot) + ': ' + name)
#0: MLRO
#1: MAO
#2: SO
#3: AOC
#4: NPP
#5: RBG

8. Operations with sequences

A sequence, is a series of items for which the relative order to one-another matters. In Python, a sequence is the parent class for strings, lists, and ranges. This way, all of these classes will share their behavior (in terms of what operations can be applied to them), but they also will have differences, which we will discuss in the coming lectures.

In [88]:
word = "Boston"
digits = [1, 2, 3, 4]
digRange = range(1, 5)
In [89]:
word
Out[89]:
'Boston'
In [90]:
digits
Out[90]:
[1, 2, 3, 4]
In [91]:
digRange
Out[91]:
range(1, 5)

Indexing: We access an element by using the [ ] operator and a number that is the index of the element in the sequence.

In [92]:
word[2]
Out[92]:
's'
In [93]:
digits[2]
Out[93]:
3
In [94]:
digRange[2]
Out[94]:
3

Finding length: Because sequences consist of zero or more items, we can use len to find how many items they contain.

In [95]:
len(word)
Out[95]:
6
In [96]:
len(digits)
Out[96]:
4
In [97]:
len(digRange)
Out[97]:
4

Concatenation: Sequences can be concatenated by using the operators '+' or '*'

In [98]:
word + " Globe"
Out[98]:
'Boston Globe'
In [99]:
digits + [4]
Out[99]:
[1, 2, 3, 4, 4]

Note that concatenation is not supported for range objects.

In [100]:
digRange + range(4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-100-e35c5d6c1483> in <module>
----> 1 digRange + range(4)

TypeError: unsupported operand type(s) for +: 'range' and 'range'
In [101]:
word * 3
Out[101]:
'BostonBostonBoston'
In [102]:
digits * 2
Out[102]:
[1, 2, 3, 4, 1, 2, 3, 4]
In [103]:
digRange * 2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-103-d7d50eb36fcd> in <module>
----> 1 digRange * 2

TypeError: unsupported operand type(s) for *: 'range' and 'int'

The membership operator in: this operator returns True when an item is part of the sequence, and False otherwise

In [104]:
't' in word
Out[104]:
True
In [105]:
'a' in word
Out[105]:
False
In [106]:
'ton' in word
Out[106]:
True
In [107]:
'bos' in word
Out[107]:
False
In [108]:
1 in digits
Out[108]:
True
In [109]:
5 in digits
Out[109]:
False
In [110]:
1 in digRange
Out[110]:
True
In [111]:
5 in digRange
Out[111]:
False
In [112]:
def isVowel2(char):
    """Use `in` for a simpler definition of isVowel"""
    return char.lower() in 'aeiou'
In [113]:
def testIsVowel2(char):
    """Helper function to test isVowel"""
    print("isVowel2('" + char + "')",  "=>",  isVowel2(char))
In [114]:
testIsVowel2('d')
testIsVowel2('D')
testIsVowel2('a')
testIsVowel2('E')
testIsVowel2('i')
testIsVowel2('O')
testIsVowel2('u')
testIsVowel2('y')
isVowel2('d') => False
isVowel2('D') => False
isVowel2('a') => True
isVowel2('E') => True
isVowel2('i') => True
isVowel2('O') => True
isVowel2('u') => True
isVowel2('y') => False

Slicing: this operation uses two indices (a start and a stop) to create a subsequence of items between the two indices.

In [115]:
word[1:4]
Out[115]:
'ost'
In [116]:
digits[1:4]
Out[116]:
[2, 3, 4]
In [117]:
digRange[1:4]
Out[117]:
range(2, 5)

If the first index is omitted, the start index is by default 0.

In [118]:
word[:3]
Out[118]:
'Bos'
In [119]:
digits[:3]
Out[119]:
[1, 2, 3]
In [120]:
digRange[:3]
Out[120]:
range(1, 4)

If the stop index is greater than the length of the sequence, Python doesn't return an error, it returns the sequence until the end.

In [121]:
word[3:10]
Out[121]:
'ton'

But, we can omit the stop index entirely and get the rest of the subsequence:

In [122]:
word[3:]
Out[122]:
'ton'

We can use a third parameter, step, with the slicing operator. Step works just like step for range, skipping a certain number of items.

In [123]:
word[0:6:2]
Out[123]:
'Bso'
In [124]:
digits[0:5:2]
Out[124]:
[1, 3]
In [125]:
digRange[0:5:2]
Out[125]:
range(1, 5, 2)

We can omit the stop argument as before, and Python automatically will look until the end of the sequence.

In [126]:
digits[0::2]
Out[126]:
[1, 3]

Reversing through slicing: because Python allows negative indexing (see slide 15), by using step -1, we can reverse a sequence.

In [127]:
word[::-1]
Out[127]:
'notsoB'
In [128]:
digits[::-1]
Out[128]:
[4, 3, 2, 1]
In [129]:
digRange[::-1]
Out[129]:
range(4, 0, -1)

From strings to lists

We can create a list from a string using the list function.

In [130]:
list(word)
Out[130]:
['B', 'o', 's', 't', 'o', 'n']

We can also produce a list by splitting a string at spaces. This is commonly use to split a sentence into a list of words.

In [131]:
phrase = "New England's weather is unpredictable."
phrase.split()
Out[131]:
['New', "England's", 'weather', 'is', 'unpredictable.']

When the split method doesn't take arguments, it splits by default at the white space. If needed, you can split at some other character.

In [132]:
message = "I woke up. The sun rose too. The sky was serene."
message.split('.')
Out[132]:
['I woke up', ' The sun rose too', ' The sky was serene', '']

Notice how the character "." was removed and the string was split exactly where the "." was.

9. Practice

Write a function that sums all the numbers in the list and returns that sum.

In [133]:
def sumListOfNums(numList):
    # Your code here
    total = 0
    for num in numList:
        total += num
    return total
In [134]:
sumListOfNums([1, 4, -2]) # should return 3
Out[134]:
3
In [135]:
sumListOfNums([1, 2, 3, 4]) # should return 10
Out[135]:
10
In [136]:
sumListOfNums([]) # should return 0
Out[136]:
0

Write a function that prints out the squares of every number from 1 to some number x.

In [137]:
def printSquares(x):
    # Your code here
    for num in range(1, x + 1):
        print(num ** 2)
In [138]:
printSquares(4) # should print 1, 4, 9, 16
1
4
9
16
In [139]:
printSquares(6) # should print 1, 4, 9, 16, 25, 36
1
4
9
16
25
36

Write a function that prints out all the odd elements in a list.

In [140]:
def printOdd(lst):
    # Your code here
    for i in range(len(lst)):
        if i % 2 == 1:
            print(lst[i])
In [141]:
printOdd([4, 'n', 28, 'o']) # should print 'n', 'o'
n
o
In [142]:
printOdd(['a', 'b']) # should print 'b'
b

Write a function that takes a list of numbers and returns the index of the last number greater than 100. If no numbers exist greater than 100, return -1.

In [143]:
def lastIndex(numList):
    # Your code here
    lastIndex = -1
    for i in range(len(numList)):
        num = numList[i]
        if num > 100:
            lastIndex = i
    return lastIndex
In [144]:
lastIndex([1, 3, 102, 4]) # should return 2
Out[144]:
2
In [145]:
lastIndex([103, 102]) # should return 1
Out[145]:
1
In [146]:
lastIndex([1, 2, 3]) # should return -1
Out[146]:
-1

Write a function that prints out all the numbers in a list that are both even and at even indices.

In [147]:
def evens(numList):
    # Your code here
    for i in range(len(numList)):
        num = numList[i]
        if i % 2 == 0 and num % 2 == 0:
            print(num)
In [148]:
evens([0, 4, 5, 7, 8]) # should print 0, 8
0
8
In [149]:
evens([1, 2, 3, 4, 5, 6]) # should not print anything
In [150]:
evens([2, 3, 4]) # should print 2, 4
2
4

Challenge

Write a function that checks whether a string is a palindrome. A palindrome is a word or phrase that reads the same backwards or forwards. The function should return True if it the string is a palindrome and False otherwise.

Note that this is pretty tricky!

In [151]:
def isPalindrome(phrase):
    # Your code here
    for i in range(len(phrase)):
        let = phrase[i]
        endLet = phrase[len(phrase) - i - 1]
        if let != endLet:
            return False
    return True
In [152]:
isPalindrome("ADA") # should return True
Out[152]:
True
In [153]:
isPalindrome("andy") # should return False
Out[153]:
False
In [154]:
isPalindrome("naan") # should return True
Out[154]:
True
In [155]:
isPalindrome("turtle") # should return False
Out[155]:
False
In [156]:
isPalindrome("able was i ere i saw elba") # should return True
Out[156]:
True
In [157]:
isPalindrome("") # should return True
Out[157]:
True