Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

23. What is the difference between an iterable and an iterator?

In Python, the terms **iterable** and **iterator** are closely related but represent distinct concepts crucial for efficient sequence processing and memory management.

1. Iterable:

  • Concept: An **iterable** is any Python object that can be "iterated over" or looped through. Essentially, if you can use an object in a `for` loop, it's an iterable.
  • Protocol: An object is iterable if it implements one of the following methods:
    • `__iter__(self)`: This method must return an **iterator** object. This is the primary way Python determines if an object is iterable.
    • `__getitem__(self, index)`: If a class implements `__getitem__` (allowing element access by index, like a list), Python can implicitly create an iterator for it.
  • Characteristics:
    • Can be iterated over multiple times.
    • They are the "containers" of items (e.g., lists, tuples, strings, dictionaries, sets, files).
    • They provide a way to *get* an iterator.
  • Analogy: Think of an iterable as a book. You can read it multiple times (iterate over it again and again). To read it, you need a bookmark (an iterator) to keep track of your current page.

2. Iterator:

  • Concept: An **iterator** is an object that represents a stream of data. It provides a way to access elements of a sequence one at a time, keeping track of the current position in the sequence.
  • Protocol: An object is an iterator if it implements the **iterator protocol**, which requires two methods:
    • `__iter__(self)`: This method must return the iterator object itself. This makes iterators also iterables, which is why they can be used directly in `for` loops.
    • `__next__(self)`: This method returns the next item from the sequence. If there are no more items, it must raise a `StopIteration` exception.
  • Characteristics:
    • They maintain state (their current position).
    • They are "exhausted" after a single pass. Once `__next__` raises `StopIteration`, you cannot get more values from that specific iterator object without creating a new one.
    • They are memory efficient because they don't store all elements in memory simultaneously; they generate or fetch them on demand.
  • Analogy: Think of an iterator as a bookmark in a book. It knows where you are (its state) and can tell you the next page (`__next__`). Once you've read all the pages with that bookmark, it's "exhausted"; you need a new bookmark to re-read the book from the beginning.

Summary Table:

Feature Iterable Iterator
Definition An object that can be looped over (e.g., list, tuple) An object that keeps track of its position and can return the next element
Required methods `__iter__()` or `__getitem__()` `__iter__()` and `__next__()`
Behavior in `for` loop A `for` loop first calls `iter()` on the iterable to get an iterator, then calls `next()` on the iterator. A `for` loop calls `next()` directly on the iterator.
Reusability Can be iterated over multiple times. Generally exhausted after a single pass. To re-iterate, you need a new iterator.
Memory May store all elements in memory (e.g., a list). Generates/fetches elements on demand; memory efficient for large datasets.
Relationship An object *from which* you can get an iterator. An object *that performs* the iteration. Every iterator is an iterable.

In short, an **iterable** is something you can loop over (like a list), and an **iterator** is what actually does the looping (it remembers its position and gives you the next item). When you use a `for` loop, Python automatically handles getting an iterator from your iterable and then calling `next()` on it.


# --- Example 1: Demonstrating Iterable vs. Iterator ---

my_list = [10, 20, 30, 40]
print(f"My list: {my_list}")
print(f"Type of my_list: {type(my_list)}") #  - This is an ITERABLE

# Step 1: Get an iterator from the iterable
my_iterator = iter(my_list)
print(f"Type of my_iterator: {type(my_iterator)}") #  - This is an ITERATOR

# Step 2: Use the iterator to get the next elements
print(f"First element using next(): {next(my_iterator)}")
print(f"Second element using next(): {next(my_iterator)}")

# You can use the remaining part of the iterator in a for loop
print("Remaining elements using for loop:")
for item in my_iterator:
    print(item)

# Attempt to get next from exhausted iterator
try:
    print(f"Attempting to get next from exhausted iterator: {next(my_iterator)}")
except StopIteration:
    print("Caught StopIteration! Iterator is exhausted.")

# The original list (iterable) is still usable
print(f"Can iterate over the original list again? Yes!")
for item in my_list:
    print(item)

# To iterate over the sequence again, you need a NEW iterator
new_iterator = iter(my_list)
print(f"Can iterate over a new iterator? Yes!")
print(list(new_iterator)) # Convert to list to show all elements


# --- Example 2: Strings are Iterables ---
print("\n--- Strings as Iterables ---")
my_string = "Python"
print(f"My string: '{my_string}'")
print(f"Type of my_string: {type(my_string)}") #  - Iterable

string_iterator = iter(my_string)
print(f"Type of string_iterator: {type(string_iterator)}") #  - Iterator

print(f"Next char: {next(string_iterator)}")
print(f"Next char: {next(string_iterator)}")

# --- Example 3: Generator as an Iterator ---
print("\n--- Generator as an Iterator ---")

def countdown(n):
    while n > 0:
        yield n
        n -= 1

# Calling the generator function returns a generator object (which is an iterator)
count_gen = countdown(3)
print(f"Type of count_gen: {type(count_gen)}") #  - This is an ITERATOR

# Can call next() on it
print(f"Next from generator: {next(count_gen)}")

# Can loop over it
print("Looping through generator:")
for num in count_gen:
    print(num)

try:
    print(f"Attempting to get next from exhausted generator: {next(count_gen)}")
except StopIteration:
    print("Caught StopIteration! Generator is exhausted.")

# To reuse, create a new generator object
print(f"New generator object: {list(countdown(2))}")
        

Explanation of the Example Code:

  • **`my_list` (Iterable):**
    • `my_list` is a Python `list`, which is an iterable. You can directly use it in a `for` loop.
    • Calling `iter(my_list)` explicitly returns a `list_iterator` object. This confirms that lists provide iterators.
  • **`my_iterator` (Iterator):**
    • `my_iterator` is the actual iterator object obtained from `my_list`. Its type is `list_iterator`.
    • `next(my_iterator)` is used to manually fetch elements one by one. Each call advances the iterator's internal state.
    • When the iterator runs out of elements, `next()` raises a `StopIteration` exception, which is how `for` loops know when to stop.
    • Crucially, once `my_iterator` is exhausted, it cannot be reused. To iterate over `my_list` again, a *new* iterator object must be created from `my_list` (e.g., `new_iterator = iter(my_list)`).
  • **Strings as Iterables:**
    • The `my_string` example shows that strings are also iterables. Calling `iter()` on a string yields a `str_iterator`, which then allows you to retrieve characters one by one.
  • **Generator as an Iterator:**
    • The `countdown` function is a **generator function** because it contains `yield`. When `countdown(3)` is called, it returns a `generator` object.
    • A generator object *is* an iterator. It automatically implements both `__iter__` and `__next__`.
    • You can call `next()` on `count_gen` or iterate over it with a `for` loop, just like any other iterator. Once exhausted, it also raises `StopIteration`.
    • This demonstrates that generators are a convenient way to create iterators without manually implementing the iterator protocol.

This example clearly differentiates iterables (what you can loop over) from iterators (what actually performs the loop, managing state and yielding values one by one), and highlights how generators simplify the creation of iterators.