<jupyter_start><jupyter_text>All the IPython Notebooks in **Python Advanced Topics** lecture series by Dr. <NAME> are available @ **[GitHub](https://github.com/milaan9/07_Python_Advanced_Topics)**  Python IteratorsIterators are objects that can be iterated upon. In this tutorial, you will learn how iterator works and how you can build your own iterator using **`__iter__`** and **`__next__`** methods.  Iterators in PythonIterators are everywhere in Python. They are elegantly implemented within `for` loops, comprehensions, generators etc. but are hidden in plain sight.Iterator in Python is simply an **[object](https://github.com/milaan9/06_Python_Object_Class/blob/main/002_Python_Classes_and_Objects.ipynb)** that can be iterated upon. An object which will return data, one element at a time.Technically speaking, a Python **iterator object** must implement two special methods, **`__iter__()`** and **`__next__()`**, collectively called the **iterator protocol**.An object is called **iterable** if we can get an iterator from it. Most built-in containers in Python like: **[string](https://github.com/milaan9/02_Python_Datatypes/blob/main/002_Python_String.ipynb)**, **[list](https://github.com/milaan9/02_Python_Datatypes/blob/main/003_Python_List.ipynb)**, **[tuple](https://github.com/milaan9/02_Python_Datatypes/blob/main/004_Python_Tuple.ipynb)** etc. are iterables.The **`iter()`** function (which in turn calls the **`__iter__()`** method) returns an iterator from them.  Iterating Through an IteratorWe use the **`next()`** function to manually iterate through all the items of an iterator. When we reach the end and there is no more data to be returned, it will raise the **`StopIteration`** Exception. Following is an example.<jupyter_code># Example 1:

# define a list
my_list = [6, 9, 0, 3]  # 4 elements

# get an iterator using iter()
my_iter = iter(my_list)

# iterate through it using next()

print(next(my_iter))       # Output: 6
print(next(my_iter))       # Output: 9

# next(obj) is same as obj.__next__()

print(my_iter.__next__())  # Output: 0
print(my_iter.__next__())  # Output: 3

# This will raise error, no items left
next(my_iter)<jupyter_output>6
9
0
3<jupyter_text>A more elegant way of automatically iterating is by using the **[for loop](https://github.com/milaan9/03_Python_Flow_Control/blob/main/005_Python_for_Loop.ipynb)**. Using this, we can iterate over any object that can return an iterator, for example list, string, file etc.<jupyter_code>for element in my_list:  # create a function
    print(element)<jupyter_output>6
9
0
3<jupyter_text>Working of `for` loop for IteratorsAs we see in the above example, the **[for loop](https://github.com/milaan9/03_Python_Flow_Control/blob/main/005_Python_for_Loop.ipynb)** was able to iterate automatically through the list.In fact the **`for`** loop can iterate over any iterable. Let's take a closer look at how the **`for`** is actually implemented in Python.```python>>> for element in iterable:>>>      do something with element```Is actually implemented as.```python>>>  create an iterator object from that iterable>>> iter_obj = iter(iterable)>>>  infinite loop>>> while True:>>>     try:>>>       get the next item>>>         element = next(iter_obj)>>>       do something with element>>>     except StopIteration:>>>       if StopIteration is raised, break from loop>>>         break```So internally, the **`for`** loop creates an iterator object, **`iter_obj`** by calling **`iter()`** on the iterable.Ironically, this **`for`** loop is actually an infinite **[while loop](https://github.com/milaan9/03_Python_Flow_Control/blob/main/006_Python_while_Loop.ipynb)**.Inside the loop, it calls **`next()`** to get the next element and executes the body of the **`for`** loop with this value. After all the items exhaust, **`StopIteration`** is raised which is internally caught and the loop ends. Note that any other kind of exception will pass through.  Building Custom IteratorsBuilding an iterator from scratch is easy in Python. We just have to implement the **`__iter__()`** and the **`__next__()`** methods.The **`__iter__()`** method returns the iterator object itself. If required, some initialization can be performed.The **`__next__()`** method must return the next item in the sequence. On reaching the end, and in subsequent calls, it must raise **`StopIteration`**.Here, we show an example that will give us the next power of 2 in each iteration. Power exponent starts from zero up to a user set number.If you do not have any idea about object-oriented programming, visit **[Python Object-Oriented Programming](https://github.com/milaan9/06_Python_Object_Class/blob/main/001_Python_OOPs_Concepts.ipynb)**.<jupyter_code>class PowTwo:
    """Class to implement an iterator
    of powers of two"""

    def __init__(self, max=0):
        self.max = max

    def __iter__(self):
        self.n = 0
        return self

    def __next__(self):
        if self.n <= self.max:
            result = 2 ** self.n
            self.n += 1
            return result
        else:
            raise StopIteration


# create an object
numbers = PowTwo(4)

# create an iterable from the object
i = iter(numbers)

# Using next to get to the next iterator element
print(next(i))
print(next(i))
print(next(i))
print(next(i))
print(next(i))<jupyter_output>1
2
4
8
16<jupyter_text>We can also use a **`for`** loop to iterate over our iterator class.<jupyter_code>for i in PowTwo(5):  # calling the class
    print(i)<jupyter_output>1
2
4
8
16
32
{'fraction_non_alphanumeric': 0.11332007952286283, 'fraction_numerical': 0.01626604012289897, 'mean_word_length': 4.606889564336373, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 53, 'https://': 9, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '83353', 'license': 'MIT', 'n_tokens_mistral': 1732, 'n_tokens_neox': 1621, 'n_words': 633, 'path': '001_Python_Iterators.ipynb', 'repo_name': 'milaan9/07_Python_Advanced_Topics', 'size': '5540'}