7  Python Iterables

In Python, an iterable is an object capable of returning its members one at a time. Common examples of iterables include strings, lists, tuples, sets, dictionaries, and Ranges. Iterables are fundamental in Python for loops, comprehensions, and many built-in functions.

7.1 What are Python Iterables

7.1.1 What Makes an Object Iterable?

An object is considered iterable if it implements the __iter__() method, which returns an iterator object, or the __getitem__() method, which allows it to be accessed sequentially.

my_list = [1, 2, 3, 4, 5]
help(my_list.__iter__)
Help on method-wrapper:

__iter__() unbound builtins.list method
    Implement iter(self).
help(my_list.__getitem__)
Help on built-in function __getitem__:

__getitem__(index, /) method of builtins.list instance
    Return self[index].

7.1.2 Checking if an Object is Iterable

You can check if an object is iterable using the collections.abc.Iterable class:

from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable))  
print(isinstance(123, Iterable))    
print(isinstance(range(5), Iterable))
print(isinstance("hello", Iterable))   
True
False
True
True

7.2 Common Iterables in Python

Here are some built-in iterables in Python:

7.2.1 Strings

for char in "Python":
    print(char, end=" ")
P y t h o n 

7.2.2 Lists

for num in [1, 2, 3]:
    print(num, end=" ")
1 2 3 

7.2.3 Tuples

for item in (4, 5, 6):
    print(item, end=" ")
4 5 6 

7.2.4 Sets

for elem in {1, 2, 3}:
    print(elem, end=" ")
1 2 3 

7.2.5 Dictionaries

my_dict = {"a": 1, "b": 2}
for key in my_dict:
    print(key, end=" ")
a b 

7.2.6 Ranges

for i in range(5):  # Generates numbers from 0 to 4
    print(i, end=" ")
0 1 2 3 4 

7.3 Iterating over Iterables

You can iterate over an iterable using:

  • A for loop.
  • the iter() and next() functions

We’ve often used for loops to traverse iterables in Python. Now, let’s dive into how you can manually iterate over an iterable using the built-in iter() and next() functions. This approach offers finer control over the iteration process and can be especially useful in more advanced scenarios.

# Example: Manually Iterating Over an Iterable Using iter() and next()

# Define an iterable, such as a list of numbers
numbers = [10, 20, 30, 40, 50]

# Obtain an iterator from the iterable
iterator = iter(numbers)

# Use a while loop to manually iterate over the elements
while True:
    try:
        # Retrieve the next element in the iterator
        number = next(iterator)
        print(number)
    except StopIteration:
        # When there are no more elements, a StopIteration exception is raised.
        print("Iteration complete!")
        break
10
20
30
40
50
Iteration complete!

7.4 Iterables Unpacking

Python supports unpacking for iterables, allowing you to assign elements of an iterable to variables in a concise and readable way. Unpacking is a powerful feature that works with python iterable objects.

7.4.1 Basic Unpacking

You can unpack the elements of an iterable into separate variables:

# Unpacking a list
numbers = [1, 2, 3]
a, b, c = numbers
print(a, b, c)  # Output: 1 2 3
1 2 3

Note: The number of variables must match the number of elements in the iterable, or Python will raise a ValueError.

7.4.2 Extended (*) Unpacking

Using the * operator, you can unpack multiple elements into a single variable, capturing the remaining elements as a list:

numbers = [1, 2, 3, 4, 5]

a, *b, c = numbers
print(a)  
print(b)  
print(c)  
1
[2, 3, 4]
5

Here, b (with *) captures all the middle elements as a list.

7.4.3 Unpacking with Functions

You can use unpacking to pass iterable elements as arguments to functions:

# unpacking a list or tuple
def add(a, b, c):
    return a + b + c

numbers = [1, 2, 3]
result = add(*numbers)
print(result)  
6
# unpacking a dictionary
def add(a=0, b=0, c=0):
    return a + b + c

numbers = {"a": 1, "b": 2, "c": 3}
result = add(**numbers)
print(result) 
6

If we are interested in retrieving only some values of the tuple, the expression *_ can be used to discard the other values. Let’s say we are interested in retrieving only the first and the last two values of the tuple:

x,*_,y,z  = (4.5, "this is a string", (("Nested tuple",5)),"99",99)
x, y , z
(4.5, '99', 99)

7.5 Built-in Functions for Iterables

Python provides a variety of built-in functions to operate on iterables, making it easy to manipulate, process, and analyze collections like lists, tuples, strings, sets, and dictionaries. Below is a list of commonly used built-in functions specifically designed for iterables.

7.5.1 General Functions

Function Description Example
len() Returns the number of elements in an iterable. len([1, 2, 3])3
min() Returns the smallest element in an iterable. min([3, 1, 4])1
max() Returns the largest element in an iterable. max([3, 1, 4])4
sum() Returns the sum of elements in an iterable (numeric types only). sum([1, 2, 3])6
sorted() Returns a sorted list from an iterable (does not modify the original). sorted([3, 1, 2])[1, 2, 3]
reversed() Returns an iterator that accesses the elements of an iterable in reverse. list(reversed([1, 2, 3]))[3, 2, 1]
enumerate() Returns an iterator of tuples containing indices and elements of the iterable. list(enumerate(['a', 'b', 'c']))[(0, 'a'), (1, 'b'), (2, 'c')]
all() Returns True if all elements of the iterable are true (or if empty). all([True, 1, 'a'])True
any() Returns True if any element of the iterable is true. any([False, 0, 'b'])True
str.join(iterable) Joins elements of an iterable (e.g., list, tuple) into a single string, using the given string as a separator. ''.join(['a', 'b', 'c'])'abc'
sorted(["apple", "orange", "banana"])
['apple', 'banana', 'orange']
max(["apple", "orange", "banana"])
'orange'

7.5.2 enumerate()

The enumerate() function adds a counter to an iterable and returns it as an enumerate object, which can be iterated over to get both the index and the value of each element in the iterable.

7.5.2.1 Syntax

enumerate(iterable, start=0)

fruits = ["apple", "banana", "cherry"]

for index, fruit in enumerate(fruits):
    print(f"Index: {index}, Fruit: {fruit}")
Index: 0, Fruit: apple
Index: 1, Fruit: banana
Index: 2, Fruit: cherry
# change the start index
fruits = ["apple", "banana", "cherry"]

for index, fruit in enumerate(fruits, start=1):
    print(f"Index: {index}, Fruit: {fruit}")
Index: 1, Fruit: apple
Index: 2, Fruit: banana
Index: 3, Fruit: cherry
# using enumerate() with a list comprehension
fruits = ["apple", "banana", "cherry"]
indexed_fruits = [(index, fruit) for index, fruit in enumerate(fruits, start=1)]
print(indexed_fruits)
[(1, 'apple'), (2, 'banana'), (3, 'cherry')]
# Working with Strings
word = "python"

for index, char in enumerate(word):
    print(f"Index: {index}, Character: {char}")
Index: 0, Character: p
Index: 1, Character: y
Index: 2, Character: t
Index: 3, Character: h
Index: 4, Character: o
Index: 5, Character: n

7.5.3 zip()

The zip() function is a built-in Python function that combines two or more iterables (e.g., lists, tuples, strings) into a single iterator of tuples. It is commonly used to pair elements from multiple iterables based on their positions.

help(zip)
Help on class zip in module builtins:

class zip(object)
 |  zip(*iterables, strict=False) --> Yield tuples until an input is exhausted.
 |
 |     >>> list(zip('abcdefg', range(3), range(4)))
 |     [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
 |
 |  The zip object yields n-length tuples, where n is the number of iterables
 |  passed as positional arguments to zip().  The i-th element in every tuple
 |  comes from the i-th iterable argument to zip().  This continues until the
 |  shortest argument is exhausted.
 |
 |  If strict is true and one of the arguments is exhausted before the others,
 |  raise a ValueError.
 |
 |  Methods defined here:
 |
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |
 |  __iter__(self, /)
 |      Implement iter(self).
 |
 |  __next__(self, /)
 |      Implement next(self).
 |
 |  __reduce__(...)
 |      Return state information for pickling.
 |
 |  __setstate__(...)
 |      Set state information for unpickling.
 |
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |
 |  __new__(*args, **kwargs)
 |      Create and return a new object.  See help(type) for accurate signature.

Key Points

  • Aggregation of Iterables:
    zip() takes any number of iterables as arguments and returns an iterator of tuples, where each tuple contains the corresponding elements from all the iterables.

  • Length of the Result:
    The iterator stops when the shortest input iterable is exhausted. If the iterables have different lengths, elements from the longer iterables that do not have a corresponding element in the shorter ones are ignored.

  • Return Type:
    The object returned by zip() is an iterator in Python 3. To convert it to a list or tuple, you can use the list() or tuple() functions.

Basic Usage Example

# combining two lists
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']

zipped = zip(list1, list2)
zipped 
<zip at 0x20e27f3cec0>
print(zipped)
print(type(zipped))

# is zipped an iterator?
print(isinstance(zipped, Iterable))
<zip object at 0x0000020E27F3CEC0>
<class 'zip'>
True

The zip() function returns an iterator, not a list. When you print an iterator directly, it doesn’t display the elements but rather the object’s memory address. To see the elements, you can iterate over it using a loop or convert the iterator into a list (or tuple or dict).

# iterate over the zip object
for item in zipped:
    print(item)
(1, 'a')
(2, 'b')
(3, 'c')
# convert the zip object to a list
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']

zipped = zip(list1, list2)
print(list(zipped))  
[(1, 'a'), (2, 'b'), (3, 'c')]
# Creating dictionaries from two list
keys = ['name', 'age', 'city']
values = ['Alice', 30, 'New York']

dictionary = dict(zip(keys, values))
print(dictionary)  
{'name': 'Alice', 'age': 30, 'city': 'New York'}
names = ['Alice', 'Bob', 'Charlie']
scores = [85, 90, 95]

for name, score in zip(names, scores):
    print(f"{name} scored {score}")
Alice scored 85
Bob scored 90
Charlie scored 95

7.5.4 unzipping

You can “unzip” a zipped object using the zip(*zipped) syntax:

zipped = [(1, 'a'), (2, 'b'), (3, 'c')]
unzipped = zip(*zipped)

list1, list2 = list(unzipped)
print(list1)  
print(list2) 
(1, 2, 3)
('a', 'b', 'c')

7.6 Practice exercises

7.6.1 Exercise 1: Iterating Using a For Loop and Manual Iteration

  1. Using a For Loop:
    • Given the list:

      numbers = [10, 20, 30, 40, 50]

      Write a for loop to print each element.

numbers = [10, 20, 30, 40, 50]

for num in numbers:
    print(num, end=" ")
10 20 30 40 50 
  1. Using iter() and next():
    • Obtain an iterator from the list using iter().
    • Use a while loop and next() to print each element.
    • Handle the StopIteration exception gracefully when there are no more elements.

Hint:
Remember that calling next() on an exhausted iterator raises a StopIteration exception.

# Create an iterator from the list
it = iter(numbers)

# Use while loop and next() to print each element
while True:
    try:
        value = next(it)
        print(value, end=" ")
    except StopIteration:
        break
10 20 30 40 50 

7.6.2 Exercise 2: Combining Iterables with zip()

  1. Zipping Two Lists:
    • Given the lists:

      list1 = [1, 2, 3]
      list2 = ['a', 'b', 'c']
    • Use the zip() function to combine these lists.

    • Convert the result into a list and print it.

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']

# Zip the two lists
zipped = zip(list1, list2)

# Convert to a list and print
zipped_list = list(zipped)
print(zipped_list)
[(1, 'a'), (2, 'b'), (3, 'c')]
  1. Iterating Over the Zipped Object:
    • Iterate over the zipped object using a for loop and print each tuple.

Note:
The zip() function returns an iterator, so converting it to a list will reveal its elements.

for pair in zipped_list:
    print(pair)  # Accessing elements of the tuple directly
(1, 'a')
(2, 'b')
(3, 'c')

7.6.3 Exercise 3: Unzipping a List of Tuples

  1. Given Data:
    • You have a list of tuples:

      pairs = [('apple', 1), ('banana', 2), ('cherry', 3)]
  2. Unzipping:
    • Use the unpacking operator (*) along with zip() to separate the list into two tuples:
      • One tuple for the fruit names.
      • One tuple for the corresponding numbers.
    • Print both tuples.

Hint:
Use the syntax names, numbers = zip(*pairs).

pairs = [('apple', 1), ('banana', 2), ('cherry', 3)]

# Unzip the list of tuples
names, numbers = zip(*pairs)

# Print the results
print("Names:", names)
print("Numbers:", numbers)
Names: ('apple', 'banana', 'cherry')
Numbers: (1, 2, 3)

7.6.4 Exercise 4: Using Built-in Functions on Iterables

  1. Numeric List Operations:
    • Create a list:

      numbers = [5, 3, 8, 1, 9]
  2. Apply Built-in Functions:
    • Use the min(), max(), and sum() functions on this list.
    • Print the results of each function.

Note:
These functions require the elements of the iterable to be of compatible types (e.g., all numbers).

numbers = [5, 3, 8, 1, 9]

# Apply built-in functions
minimum = min(numbers)
maximum = max(numbers)
total = sum(numbers)

# Print the results
print("Minimum:", minimum)
print("Maximum:", maximum)
print("Sum:", total)
Minimum: 1
Maximum: 9
Sum: 26