DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Week-03 Task-2: Iterators, Generators & Context Managers

1. Iterator Protocol (__iter__() / __next__())

What is an Iterator?

An iterator is an object that gives us values one at a time.

Example:

numbers = [10, 20, 30]

it = iter(numbers)

print(next(it))
print(next(it))
print(next(it))
Enter fullscreen mode Exit fullscreen mode

Output:

10
20
30
Enter fullscreen mode Exit fullscreen mode

If we call next() again:

print(next(it))
Enter fullscreen mode Exit fullscreen mode

Python raises:

StopIteration
Enter fullscreen mode Exit fullscreen mode

because there are no more values.

Important Functions

iter() → gets an iterator.

it = iter(numbers)
Enter fullscreen mode Exit fullscreen mode

next() → gets the next value.

next(it)
Enter fullscreen mode Exit fullscreen mode

Iterator Protocol

A custom iterator follows two special methods:

__iter__()
__next__()
Enter fullscreen mode Exit fullscreen mode
  • __iter__() → returns the iterator.
  • __next__() → returns the next value.
  • When there are no more values, __next__() raises StopIteration.

Simple Custom Iterator

class MyNumbers:

    def __init__(self):
        self.number = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.number <= 3:
            value = self.number
            self.number += 1
            return value
        else:
            raise StopIteration


numbers = MyNumbers()

print(next(numbers))
print(next(numbers))
print(next(numbers))
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
Enter fullscreen mode Exit fullscreen mode

Iterable vs Iterator

Iterable: Something that can be looped over.

Examples:

list
tuple
string
dictionary
set
Enter fullscreen mode Exit fullscreen mode

Iterator: The object that actually gives the values one by one using next().

Simple flow:

Iterable
   ↓
iter()
   ↓
Iterator
   ↓
next()
   ↓
Value
Enter fullscreen mode Exit fullscreen mode

How does a for loop work?

When we write:

for number in [10, 20, 30]:
    print(number)
Enter fullscreen mode Exit fullscreen mode

Python internally uses the iterator protocol.

Conceptually:

iterator = iter([10, 20, 30])

while True:
    try:
        number = next(iterator)
        print(number)
    except StopIteration:
        break
Enter fullscreen mode Exit fullscreen mode

So a for loop automatically calls iter() and next() for us.

Real-world use cases

Iterators are useful when working with:

  • Large datasets
  • Files
  • Database results
  • API results
  • Data pipelines
  • Streaming data

They allow us to process data one item at a time instead of handling everything at once.


2. Generator Functions & yield

What is a Generator?

A generator is an easy way to create an iterator.

Generators produce values one at a time instead of creating all the values at once.

They are created using the yield keyword.

Simple Generator

def numbers():
    yield 1
    yield 2
    yield 3
Enter fullscreen mode Exit fullscreen mode

Create a generator:

gen = numbers()
Enter fullscreen mode Exit fullscreen mode

Get values one at a time:

print(next(gen))
print(next(gen))
print(next(gen))
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
Enter fullscreen mode Exit fullscreen mode

Another next() gives:

StopIteration
Enter fullscreen mode Exit fullscreen mode

because the generator is finished.


yield vs return

return

return gives a value and ends the function.

def test():
    return 10
Enter fullscreen mode Exit fullscreen mode

After return, the function is finished.

yield

yield gives a value and pauses the function.

The function remembers where it stopped and continues from there when next() is called again.

def numbers():
    yield 10
    yield 20
    yield 30
Enter fullscreen mode Exit fullscreen mode

The flow is:

yield 10
   ↓
pause
   ↓
yield 20
   ↓
pause
   ↓
yield 30
   ↓
pause
   ↓
finished
Enter fullscreen mode Exit fullscreen mode

So remember:

return → give value and finish

yield → give value and pause


Generators are Lazy

Generators use lazy evaluation.

This means values are produced only when they are needed.

Example:

def numbers():
    print("Generating 1")
    yield 1

    print("Generating 2")
    yield 2
Enter fullscreen mode Exit fullscreen mode

When we write:

gen = numbers()
Enter fullscreen mode Exit fullscreen mode

the function does not immediately run.

When we write:

print(next(gen))
Enter fullscreen mode Exit fullscreen mode

it starts running and produces:

Generating 1
1
Enter fullscreen mode Exit fullscreen mode

This makes generators useful for large amounts of data because we don't need to store everything in memory at once.


Generator with a Loop

def numbers():
    for i in range(1, 6):
        yield i


for number in numbers():
    print(number)
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Real-world Generator Use Case

Suppose a file has millions of lines.

Instead of loading everything into memory, we can process one line at a time:

def read_lines(file):
    for line in file:
        yield line
Enter fullscreen mode Exit fullscreen mode

Then:

for line in read_lines(file):
    process(line)
Enter fullscreen mode Exit fullscreen mode

This is useful for:

  • Large files
  • Large datasets
  • Logs
  • Database records
  • API data
  • Data processing pipelines

3. Generator Expressions

A generator expression is a short way to create a generator.

It looks similar to list comprehension.

List Comprehension

numbers = [x * 2 for x in range(5)]
Enter fullscreen mode Exit fullscreen mode

This creates a list immediately:

[0, 2, 4, 6, 8]
Enter fullscreen mode Exit fullscreen mode

Generator Expression

numbers = (x * 2 for x in range(5))
Enter fullscreen mode Exit fullscreen mode

This creates a generator.

Values are produced when needed.

print(next(numbers))
print(next(numbers))
Enter fullscreen mode Exit fullscreen mode

Output:

0
2
Enter fullscreen mode Exit fullscreen mode

Easy Difference

[x * 2 for x in range(5)]
Enter fullscreen mode Exit fullscreen mode

→ List → values are created immediately.

(x * 2 for x in range(5))
Enter fullscreen mode Exit fullscreen mode

→ Generator → values are produced one at a time.

Simple memory trick:

[ ] → List

( ) → Generator expression
Enter fullscreen mode Exit fullscreen mode

4. itertools Basics

itertools is a Python standard library module that provides useful tools for working with iterators.

Import it using:

import itertools
Enter fullscreen mode Exit fullscreen mode

Some important functions are:

itertools.count()

Produces numbers continuously.

import itertools

numbers = itertools.count(1)

print(next(numbers))
print(next(numbers))
print(next(numbers))
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
Enter fullscreen mode Exit fullscreen mode

It keeps going:

4
5
6
7
...
Enter fullscreen mode Exit fullscreen mode

itertools.cycle()

Repeats a sequence continuously.

import itertools

colors = itertools.cycle(["red", "blue", "green"])

print(next(colors))
print(next(colors))
print(next(colors))
print(next(colors))
print(next(colors))
Enter fullscreen mode Exit fullscreen mode

Output:

red
blue
green
red
blue
Enter fullscreen mode Exit fullscreen mode

itertools.repeat()

Repeats the same value.

import itertools

values = itertools.repeat("Hello", 3)

for value in values:
    print(value)
Enter fullscreen mode Exit fullscreen mode

Output:

Hello
Hello
Hello
Enter fullscreen mode Exit fullscreen mode

itertools.chain()

Combines multiple iterables into one sequence.

import itertools

list1 = [1, 2, 3]
list2 = [4, 5, 6]

result = itertools.chain(list1, list2)

for number in result:
    print(number)
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
4
5
6
Enter fullscreen mode Exit fullscreen mode

itertools.islice()

Takes a limited portion from an iterator.

import itertools

numbers = itertools.count(1)

result = itertools.islice(numbers, 5)

for number in result:
    print(number)
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Important itertools functions to remember

Function Meaning
count() Keep counting
cycle() Repeat a sequence
repeat() Repeat one value
chain() Join iterables
islice() Take a portion of an iterator

5. Context Manager Protocol (__enter__() / __exit__())

What is a Context Manager?

A context manager manages setup and cleanup around a block of code.

The easiest way to understand it is:

Setup
  ↓
Do the work
  ↓
Cleanup
Enter fullscreen mode Exit fullscreen mode

A common example is working with files.

Without a context manager:

file = open("data.txt", "r")

content = file.read()

file.close()
Enter fullscreen mode Exit fullscreen mode

We have to manually close the file.

With a context manager:

with open("data.txt", "r") as file:
    content = file.read()
Enter fullscreen mode Exit fullscreen mode

Python automatically handles the cleanup.


The with Statement

The basic syntax is:

with something:
    # code
Enter fullscreen mode Exit fullscreen mode

For example:

with open("data.txt") as file:
    print(file.read())
Enter fullscreen mode Exit fullscreen mode

The basic flow is:

Open / Setup
     ↓
__enter__()
     ↓
Use the resource
     ↓
__exit__()
     ↓
Cleanup
Enter fullscreen mode Exit fullscreen mode

Context Manager Protocol

A context manager uses two special methods:

__enter__()
__exit__()
Enter fullscreen mode Exit fullscreen mode

__enter__()

Runs when we enter the with block.

Usually handles setup.

Examples:

  • Open a resource
  • Create a connection
  • Acquire a lock

__exit__()

Runs when we leave the with block.

Usually handles cleanup.

Examples:

  • Close a file
  • Close a database connection
  • Release a lock
  • Clean up resources

Simple Custom Context Manager

class MyContext:

    def __enter__(self):
        print("Entering")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting")


with MyContext():
    print("Inside")
Enter fullscreen mode Exit fullscreen mode

Output:

Entering
Inside
Exiting
Enter fullscreen mode Exit fullscreen mode

The order is:

__enter__()
    ↓
Code inside with
    ↓
__exit__()
Enter fullscreen mode Exit fullscreen mode

Using as

__enter__() can return a value.

Example:

class MyContext:

    def __enter__(self):
        print("Entering")
        return "Hello"

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting")


with MyContext() as value:
    print(value)
Enter fullscreen mode Exit fullscreen mode

Output:

Entering
Hello
Exiting
Enter fullscreen mode Exit fullscreen mode

Whatever __enter__() returns is stored in the variable after as.

So:

with MyContext() as value:
Enter fullscreen mode Exit fullscreen mode

means:

__enter__()
    ↓
returned value
    ↓
value
Enter fullscreen mode Exit fullscreen mode

What are exc_type, exc_value, and traceback?

A normal __exit__() looks like:

def __exit__(self, exc_type, exc_value, traceback):
    ...
Enter fullscreen mode Exit fullscreen mode

These parameters provide information about an exception if one occurs.

  • exc_type → type of the exception
  • exc_value → exception details/message
  • traceback → information about where the exception occurred

If there is no exception, they are generally:

None
None
None
Enter fullscreen mode Exit fullscreen mode

Context Managers and Errors

One important feature is that __exit__() gets called even if an exception occurs inside the with block.

Example:

class MyContext:

    def __enter__(self):
        print("Entering")

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting")


with MyContext():
    print("Inside")
    x = 10 / 0
Enter fullscreen mode Exit fullscreen mode

The __exit__() method still gets a chance to run.

This makes context managers useful for cleanup even when something goes wrong.


Real-world Uses of Context Managers

Context managers are commonly used for:

  • Files
  • Database connections
  • Locks
  • Network resources
  • Temporary resources
  • Transactions

The common pattern is:

Setup
  ↓
Use
  ↓
Cleanup
Enter fullscreen mode Exit fullscreen mode

6. Iterator vs Generator vs Context Manager

Concept Main Purpose Important Methods/Keyword
Iterator Give values one at a time __iter__(), __next__()
Generator Easily create an iterator yield
Generator Expression Short way to create a generator ( )
itertools Ready-made iterator tools count(), cycle(), chain() etc.
Context Manager Manage setup and cleanup __enter__(), __exit__()

7. Easy Interview Explanations

What is an Iterator?

"An iterator is an object that gives values one at a time. It follows the iterator protocol using __iter__() and __next__(). __next__() returns the next value and raises StopIteration when there are no more values."

What is a Generator?

"A generator is an easy way to create an iterator. It uses the yield keyword to produce values one at a time. yield pauses the function and remembers its state so it can continue later."

What is a Generator Expression?

"A generator expression is a compact way to create a generator using parentheses. It produces values lazily instead of creating the entire collection immediately."

Example:

(x * 2 for x in range(5))
Enter fullscreen mode Exit fullscreen mode

What is itertools?

"itertools is a Python standard library module that provides ready-made tools for working with iterators efficiently."

What is a Context Manager?

"A context manager manages setup and cleanup around a block of code. It uses the context manager protocol with __enter__() and __exit__(), usually through the with statement."


8. Final Quick Revision

Iterator

Gives values one by one
        ↓
__iter__()
__next__()
        ↓
StopIteration when finished
Enter fullscreen mode Exit fullscreen mode

Generator

Easy way to create an iterator
        ↓
yield
        ↓
Produces values one at a time
        ↓
Lazy / memory efficient
Enter fullscreen mode Exit fullscreen mode

Generator Expression

(x for x in range(5))
Enter fullscreen mode Exit fullscreen mode

Short way to create a generator.

itertools

Ready-made iterator tools
        ↓
count()
cycle()
repeat()
chain()
islice()
Enter fullscreen mode Exit fullscreen mode

Context Manager

Setup
  ↓
__enter__()
  ↓
Work
  ↓
__exit__()
  ↓
Cleanup
Enter fullscreen mode Exit fullscreen mode

Usually used with:

with:
Enter fullscreen mode Exit fullscreen mode

⭐ The 3 Most Important Lines to Remember

ITERATOR  → __iter__() + __next__()
GENERATOR → yield
CONTEXT MANAGER → with + __enter__() + __exit__()
Enter fullscreen mode Exit fullscreen mode

Super-simple explanation:

Iterator: "Give me the next value."

Generator: "I'll give you values one at a time using yield."

Context Manager: "I'll set things up, let you do your work, and clean things up when you're done."

Top comments (0)