Tutorials Logic, IN info@tutorialslogic.com

Iterators Generators in Python yield

Python Iterators

An iterator gives values one at a time with next(), and a for loop uses that protocol automatically.

A generator is a simple way to create an iterator with yield instead of building a full list in memory.

Iterators and generators are useful when data is large, produced slowly, or should be processed step by step.

Iterators

An iterator is any object that implements __iter__() and __next__(). When you use a for loop, Python calls these methods behind the scenes.

Manual next and for-loop Iteration

Manual next and for-loop Iteration
# Built-in iterables
nums = [1, 2, 3]
it = iter(nums)          # get iterator from list
print(next(it))          # 1
print(next(it))          # 2
print(next(it))          # 3
# next(it)               # StopIteration!

# for loop does this automatically
for n in [1, 2, 3]:
    print(n)

# Custom iterator class
class CountUp:
    def __init__(self, start: int, stop: int):
        self.current = start
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.stop:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

for n in CountUp(1, 5):
    print(n)   # 1 2 3 4 5

Generators

A generator is a function that uses yield instead of return. It produces values one at a time, pausing between each - memory-efficient for large sequences.

Generator Functions

Generator Functions
def count_up(start: int, stop: int):
    current = start
    while current <= stop:
        yield current        # pause here, return value
        current += 1         # resume here on next call

gen = count_up(1, 5)
print(next(gen))   # 1
print(next(gen))   # 2

for n in count_up(1, 5):
    print(n)       # 1 2 3 4 5

# Fibonacci generator - infinite sequence
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Generator vs list - memory comparison
import sys
gen_size  = sys.getsizeof(x for x in range(1_000_000))
list_size = sys.getsizeof([x for x in range(1_000_000)])
print(f"Generator: {gen_size} bytes")    # ~200 bytes
print(f"List:      {list_size} bytes")   # ~8 MB!

Generator Expressions

Like list comprehensions but with parentheses - lazy evaluation, no memory overhead.

Lazy Squares Without a List

Lazy Squares Without a List
# List comprehension - creates entire list in memory
squares_list = [x**2 for x in range(10)]

# Generator expression - lazy, one value at a time
squares_gen = (x**2 for x in range(10))

# Use directly in functions
total = sum(x**2 for x in range(1, 101))   # sum of squares 1-100
print(total)   # 338350

# Find first match without building full list
first_even = next(x for x in range(100) if x % 7 == 0 and x > 10)
print(first_even)   # 14

# Chain generators (pipeline)
numbers = range(1, 20)
evens = (x for x in numbers if x % 2 == 0)
squared = (x**2 for x in evens)
print(list(squared))  # [4, 16, 36, 64, 100, 144, 196, 256, 324]

yield from

Flatten Nested Lists with yield from

Flatten Nested Lists with yield from
# yield from - delegate to another iterable
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)   # recurse into sub-lists
        else:
            yield item

data = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(data)))   # [1, 2, 3, 4, 5, 6, 7]

# Combine multiple generators
def chain(*iterables):
    for it in iterables:
        yield from it

result = list(chain([1, 2], [3, 4], [5, 6]))
print(result)   # [1, 2, 3, 4, 5, 6]

# itertools - powerful iterator tools
import itertools

# islice - take first n items from any iterable
fib_10 = list(itertools.islice(fibonacci(), 10))
print(fib_10)   # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# chain - combine iterables
combined = list(itertools.chain([1, 2], [3, 4], [5]))
print(combined)  # [1, 2, 3, 4, 5]
Iterator and generator check

Can You Produce Values Lazily?

5 checks
  • An iterator is any object that implements __iter__() and __next__().
  • When you use a for loop, Python calls these methods behind the scenes.
  • A generator is a function that uses yield instead of return.
  • It produces values one at a time, pausing between each - memory-efficient for large sequences.
  • A generator expression uses parentheses and produces values lazily instead of constructing a complete list first.

Iterator State Surprises

  • Expecting a generator to restart automatically

    Create a new generator when you need to loop over the values again.
  • Using a generator when a list is clearer

    Use lists for small reusable data. Use generators for lazy sequences or large streams.
  • Forgetting yield pauses state

    Remember that each next() resumes the function after the previous yield.

Try this next

Yield Values One at a Time

0 of 3 completed

  1. Write a generator that yields even numbers up to a limit.
  2. Loop over a generator twice and explain why the second loop is empty.
  3. Turn a generator into a list only when the data is small enough.

Questions About Iterator Generator

An iterable can produce an iterator with iter(). An iterator returns values one at a time with next().

Use a generator when values can be produced one at a time instead of building a whole list first.

Generators are consumed as they produce values. Create a new generator if you need to loop again.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.