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))
Output:
10
20
30
If we call next() again:
print(next(it))
Python raises:
StopIteration
because there are no more values.
Important Functions
iter() → gets an iterator.
it = iter(numbers)
next() → gets the next value.
next(it)
Iterator Protocol
A custom iterator follows two special methods:
__iter__()
__next__()
-
__iter__()→ returns the iterator. -
__next__()→ returns the next value. - When there are no more values,
__next__()raisesStopIteration.
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))
Output:
1
2
3
Iterable vs Iterator
Iterable: Something that can be looped over.
Examples:
list
tuple
string
dictionary
set
Iterator: The object that actually gives the values one by one using next().
Simple flow:
Iterable
↓
iter()
↓
Iterator
↓
next()
↓
Value
How does a for loop work?
When we write:
for number in [10, 20, 30]:
print(number)
Python internally uses the iterator protocol.
Conceptually:
iterator = iter([10, 20, 30])
while True:
try:
number = next(iterator)
print(number)
except StopIteration:
break
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
Create a generator:
gen = numbers()
Get values one at a time:
print(next(gen))
print(next(gen))
print(next(gen))
Output:
1
2
3
Another next() gives:
StopIteration
because the generator is finished.
yield vs return
return
return gives a value and ends the function.
def test():
return 10
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
The flow is:
yield 10
↓
pause
↓
yield 20
↓
pause
↓
yield 30
↓
pause
↓
finished
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
When we write:
gen = numbers()
the function does not immediately run.
When we write:
print(next(gen))
it starts running and produces:
Generating 1
1
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)
Output:
1
2
3
4
5
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
Then:
for line in read_lines(file):
process(line)
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)]
This creates a list immediately:
[0, 2, 4, 6, 8]
Generator Expression
numbers = (x * 2 for x in range(5))
This creates a generator.
Values are produced when needed.
print(next(numbers))
print(next(numbers))
Output:
0
2
Easy Difference
[x * 2 for x in range(5)]
→ List → values are created immediately.
(x * 2 for x in range(5))
→ Generator → values are produced one at a time.
Simple memory trick:
[ ] → List
( ) → Generator expression
4. itertools Basics
itertools is a Python standard library module that provides useful tools for working with iterators.
Import it using:
import itertools
Some important functions are:
itertools.count()
Produces numbers continuously.
import itertools
numbers = itertools.count(1)
print(next(numbers))
print(next(numbers))
print(next(numbers))
Output:
1
2
3
It keeps going:
4
5
6
7
...
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))
Output:
red
blue
green
red
blue
itertools.repeat()
Repeats the same value.
import itertools
values = itertools.repeat("Hello", 3)
for value in values:
print(value)
Output:
Hello
Hello
Hello
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)
Output:
1
2
3
4
5
6
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)
Output:
1
2
3
4
5
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
A common example is working with files.
Without a context manager:
file = open("data.txt", "r")
content = file.read()
file.close()
We have to manually close the file.
With a context manager:
with open("data.txt", "r") as file:
content = file.read()
Python automatically handles the cleanup.
The with Statement
The basic syntax is:
with something:
# code
For example:
with open("data.txt") as file:
print(file.read())
The basic flow is:
Open / Setup
↓
__enter__()
↓
Use the resource
↓
__exit__()
↓
Cleanup
Context Manager Protocol
A context manager uses two special methods:
__enter__()
__exit__()
__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")
Output:
Entering
Inside
Exiting
The order is:
__enter__()
↓
Code inside with
↓
__exit__()
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)
Output:
Entering
Hello
Exiting
Whatever __enter__() returns is stored in the variable after as.
So:
with MyContext() as value:
means:
__enter__()
↓
returned value
↓
value
What are exc_type, exc_value, and traceback?
A normal __exit__() looks like:
def __exit__(self, exc_type, exc_value, traceback):
...
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
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
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
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))
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
Generator
Easy way to create an iterator
↓
yield
↓
Produces values one at a time
↓
Lazy / memory efficient
Generator Expression
(x for x in range(5))
Short way to create a generator.
itertools
Ready-made iterator tools
↓
count()
cycle()
repeat()
chain()
islice()
Context Manager
Setup
↓
__enter__()
↓
Work
↓
__exit__()
↓
Cleanup
Usually used with:
with:
⭐ The 3 Most Important Lines to Remember
ITERATOR → __iter__() + __next__()
GENERATOR → yield
CONTEXT MANAGER → with + __enter__() + __exit__()
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)