Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • 📝 Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • 📝 Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • 📝 Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • 📝 Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • 📝 While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • 📝 For Each Assignments
    • 📝 All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • 📝 Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • 📝 Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • 📝 Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • 📝 Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • 📝 Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • 📝 Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • 📝 Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • 📝 Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • 📝 Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • 📝 Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • 📝 Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • 📝 Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • 📝 Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • 📝 Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • 📝 Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • 📝 Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • 📝 Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • 📝 Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • 📝 Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Generators

Python Generators - Complete Guide

Learn how to create and use generators in Python.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What are Generators?
  • The yield Keyword
  • Generator Expressions
  • Generators vs Iterators
  • Advanced Generator Features
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • What are generators — functions that produce values on demand
  • The yield keyword — how generators work
  • Generator expressions — compact generator syntax
  • Generators vs iterators — understanding the differences
  • Advanced features — send(), throw(), and close()
  • Real-world use — practical examples you can use

What are Generators?

A generator is a special type of function that produces a sequence of values on demand, one at a time. Instead of returning a single value and ending, a generator yields values one by one, pausing between each yield.

Think of a generator like a vending machine that makes one item at a time. You press a button, it makes one item and gives it to you. Then you press again, it makes another. It doesn't make everything at once — it makes things as you ask for them.

Generators are memory efficient because they don't store all values in memory at once. They're perfect for working with large datasets, streaming data, or infinite sequences.

💡 Key concept: A generator function uses yield instead of return. When called, it returns a generator object that can be iterated over.

The yield Keyword

1

How yield Works

The yield keyword is what makes a function a generator. Here's how it works:

  • When the generator is called, it returns a generator object (not the value)
  • When next() is called on the generator, it runs until it hits yield
  • The value after yield is returned to the caller
  • The generator's state is paused and remembered
  • When next() is called again, it resumes from where it paused
# The yield Keyword

print("=" * 50)
print("THE yield KEYWORD")
print("=" * 50)

# ============================================================
# A SIMPLE GENERATOR
# ============================================================

def simple_generator():
    """A simple generator that yields three values"""
    print("   Starting generator")
    yield 1
    print("   After first yield")
    yield 2
    print("   After second yield")
    yield 3
    print("   After third yield — generator is done")

print("\n1. CREATING THE GENERATOR")
gen = simple_generator()
print(f"   Generator object: {gen}")
print(f"   Type: {type(gen)}")

print("\n2. USING next() TO GET VALUES")
print(f"   next(gen): {next(gen)}")
print(f"   next(gen): {next(gen)}")
print(f"   next(gen): {next(gen)}")

print("\n3. TRYING TO GET MORE (StopIteration)")
try:
    next(gen)
except StopIteration:
    print("   StopIteration raised — generator is exhausted")


# ============================================================
# GENERATOR WITH A LOOP
# ============================================================

print("\n4. GENERATOR WITH A LOOP")

def countdown(n):
    """Count down from n to 1 using a generator"""
    print(f"   Starting countdown from {n}")
    while n > 0:
        yield n
        n -= 1
    print("   Countdown complete!")

print("   Countdown from 5:")
for num in countdown(5):
    print(f"      {num}")


# ============================================================
# UNDERSTANDING THE FLOW
# ============================================================

print("\n5. UNDERSTANDING THE FLOW")

def demo_flow():
    """Demonstrates the flow of a generator"""
    print("   Step 1: Before first yield")
    yield "First value"
    print("   Step 2: After first yield, before second")
    yield "Second value"
    print("   Step 3: After second yield, before third")
    yield "Third value"
    print("   Step 4: Generator finishing")

flow_gen = demo_flow()
print("   Creating generator...")
print(f"   {next(flow_gen)}")
print(f"   {next(flow_gen)}")
print(f"   {next(flow_gen)}")

print("\n   Notice how the print statements appear between yields!")
print("   The generator pauses and resumes, maintaining its state.")


# ============================================================
# GENERATORS ARE LAZY
# ============================================================

print("\n6. GENERATORS ARE LAZY")

def lazy_generator():
    """Generator that shows lazy evaluation"""
    print("   Generating value 1...")
    yield 1
    print("   Generating value 2...")
    yield 2
    print("   Generating value 3...")
    yield 3

print("   Creating lazy generator...")
lazy = lazy_generator()
print("   Values are only generated when asked for!")
print(f"   Getting first value: {next(lazy)}")
print(f"   Getting second value: {next(lazy)}")

The yield keyword key points:

  • Pauses execution — the generator pauses at yield
  • Remembers state — all local variables are preserved
  • Resumes on next() — continues from where it paused
  • Lazy evaluation — values are generated on demand
  • StopIteration — raised when the generator is exhausted

Quick Check: What happens when a generator hits a yield statement? (Answer: It pauses, returns the value, and remembers its state)

Generator Expressions

2

Compact Generator Syntax

A generator expression is a compact way to create a generator. It looks like a list comprehension but uses parentheses () instead of brackets [].

Generator expressions are memory efficient because they don't create a list in memory. They generate values on the fly, just like generator functions.

# Generator Expressions

print("=" * 50)
print("GENERATOR EXPRESSIONS")
print("=" * 50)

# ============================================================
# BASIC GENERATOR EXPRESSION
# ============================================================

print("\n1. BASIC GENERATOR EXPRESSION")

# Generator expression (lazy)
squares = (x ** 2 for x in range(5))
print(f"   Generator: {squares}")
print(f"   Type: {type(squares)}")

print("   Values from generator:")
for num in squares:
    print(f"      {num}")

# Compare with list comprehension (eager)
squares_list = [x ** 2 for x in range(5)]
print(f"   List comprehension: {squares_list}")
print(f"   Memory: list stores all values, generator stores none")


# ============================================================
# GENERATOR EXPRESSIONS WITH CONDITIONS
# ============================================================

print("\n2. GENERATOR EXPRESSIONS WITH CONDITIONS")

numbers = range(10)
even_squares = (x ** 2 for x in numbers if x % 2 == 0)
print("   Squares of even numbers:")
for num in even_squares:
    print(f"      {num}")


# ============================================================
# GENERATOR EXPRESSIONS ARE SINGLE-USE
# ============================================================

print("\n3. GENERATOR EXPRESSIONS ARE SINGLE-USE")

gen = (x for x in range(3))
print("   First iteration:")
for num in gen:
    print(f"      {num}")

print("   Second iteration (empty):")
for num in gen:
    print(f"      {num}")  # Nothing printed — generator is exhausted


# ============================================================
# GENERATOR EXPRESSIONS WITH FUNCTIONS
# ============================================================

print("\n4. GENERATOR EXPRESSIONS WITH FUNCTIONS")

# Use with sum() — no list created
total = sum(x ** 2 for x in range(10))
print(f"   Sum of squares (0-9): {total}")

# Use with max()
max_value = max(x for x in range(100) if x % 7 == 0)
print(f"   Largest multiple of 7 under 100: {max_value}")

# Use with any()
has_even = any(x % 2 == 0 for x in range(10))
print(f"   Any even numbers in 0-9: {has_even}")


# ============================================================
# NESTED GENERATOR EXPRESSIONS
# ============================================================

print("\n5. NESTED GENERATOR EXPRESSIONS")

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Flatten a matrix using nested generator expression
flat = (num for row in matrix for num in row)
print("   Flattened matrix:")
for num in flat:
    print(f"      {num}")

Generator expressions key points:

  • Parentheses — use () instead of []
  • Lazy evaluation — values generated on demand
  • Memory efficient — doesn't store all values
  • Single-use — once exhausted, can't be reused
  • Great with functions — works with sum(), max(), any(), etc.

Quick Check: What's the difference between a generator expression and a list comprehension? (Answer: A generator expression uses parentheses and is lazy; a list comprehension uses brackets and creates a list immediately)

Generators vs Iterators

3

Understanding the Differences

Generators and iterators are closely related, but they're not the same thing. Here's the difference:

  • Iterator — an object that implements __iter__() and __next__()
  • Generator — a function that uses yield to produce values
  • All generators are iterators — but not all iterators are generators
# Generators vs Iterators

print("=" * 50)
print("GENERATORS vs ITERATORS")
print("=" * 50)

# ============================================================
# CREATING AN ITERATOR (the hard way)
# ============================================================

print("\n1. ITERATOR — The Hard Way")

class CountIterator:
    """A custom iterator (more code to write)"""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

print("   Iterator class:")
iter_count = CountIterator(0, 5)
for num in iter_count:
    print(f"      {num}")


# ============================================================
# CREATING A GENERATOR (the easy way)
# ============================================================

print("\n2. GENERATOR — The Easy Way")

def count_generator(start, end):
    """A generator (less code to write)"""
    current = start
    while current < end:
        yield current
        current += 1

print("   Generator function:")
gen_count = count_generator(0, 5)
for num in gen_count:
    print(f"      {num}")


# ============================================================
# COMPARISON
# ============================================================

print("\n3. COMPARISON")

print("""
┌─────────────────────┬──────────────────────┬─────────────────────────────┐
│ FEATURE             │ ITERATOR             │ GENERATOR                   │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ How to create       │ Class with __iter__  │ Function with yield         │
│                     │ and __next__         │                             │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ Code amount         │ More code            │ Less code                   │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ State management    │ Manual (instance     │ Automatic (local variables) │
│                     │ variables)           │                             │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ Lazy evaluation     │ Yes                  │ Yes                         │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ Memory efficient    │ Yes                  │ Yes                         │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ Can be reused       │ Depends on design    │ No (single-use)             │
├─────────────────────┼──────────────────────┼─────────────────────────────┤
│ Use case            │ Complex state,       │ Simple sequences,           │
│                     │ multiple methods     │ data processing             │
└─────────────────────┴──────────────────────┴─────────────────────────────┘

"Generators are iterators, but with less code and automatic state management."
""")


# ============================================================
# VERIFYING: A GENERATOR IS AN ITERATOR
# ============================================================

print("\n4. VERIFYING: A GENERATOR IS AN ITERATOR")

def test_gen():
    yield 1
    yield 2
    yield 3

gen = test_gen()
print(f"   Generator has __iter__: {hasattr(gen, '__iter__')}")
print(f"   Generator has __next__: {hasattr(gen, '__next__')}")
print(f"   Generator is an iterator: {isinstance(gen, iter)}")
print(f"   Generator is an iterable: {hasattr(gen, '__iter__')}")

Generators vs iterators key points:

  • Generators are simpler — less code than iterators
  • Automatic state — no need for instance variables
  • All generators are iterators — but not vice versa
  • Single-use — generators are exhausted after iteration
  • Use generators — for simple sequences and data processing

Quick Check: Are all generators iterators? (Answer: Yes — generators implement the iterator protocol automatically)

Advanced Generator Features

4

send(), throw(), and close()

Generators have some advanced features that give you more control:

  • send() — sends a value into the generator
  • throw() — raises an exception inside the generator
  • close() — stops the generator
# Advanced Generator Features

print("=" * 50)
print("ADVANCED GENERATOR FEATURES")
print("=" * 50)

# ============================================================
# send() — Sending Values Into a Generator
# ============================================================

print("\n1. send() — Sending Values")

def interactive_generator():
    """Generator that receives values"""
    print("   Generator started")
    
    # Get initial value
    x = yield "Send me a value"
    print(f"   Received: {x}")
    
    # Get another value
    y = yield "Send me another value"
    print(f"   Received: {y}")
    
    # Return the result
    result = x + y
    yield f"Result: {result}"

print("   Using send():")
gen = interactive_generator()

# Start the generator
print(f"   {next(gen)}")

# Send values
print(f"   {gen.send(10)}")
print(f"   {gen.send(20)}")
print(f"   {next(gen)}")


# ============================================================
# throw() — Raising Exceptions
# ============================================================

print("\n2. throw() — Raising Exceptions")

def exception_generator():
    """Generator that handles exceptions"""
    try:
        yield "Running normally"
        yield "Still running"
    except ValueError as e:
        yield f"Caught ValueError: {e}"
    except Exception as e:
        yield f"Caught exception: {e}"
    
    yield "Generator continuing"

print("   Using throw():")
eg = exception_generator()
print(f"   {next(eg)}")
print(f"   {next(eg)}")
print(f"   {eg.throw(ValueError('Something went wrong'))}")
print(f"   {next(eg)}")


# ============================================================
# close() — Closing a Generator
# ============================================================

print("\n3. close() — Closing a Generator")

def long_running_generator():
    """A generator that would run for a long time"""
    count = 0
    while True:
        yield f"Value: {count}"
        count += 1

print("   Starting generator...")
lr = long_running_generator()
print(f"   {next(lr)}")
print(f"   {next(lr)}")
print(f"   {next(lr)}")

print("   Closing generator...")
lr.close()

try:
    print(f"   {next(lr)}")
except StopIteration:
    print("   Generator is closed — StopIteration raised")


# ============================================================
# yield from — Delegating to Another Generator
# ============================================================

print("\n4. yield from — Delegating")

def sub_generator():
    """A sub-generator"""
    yield "A"
    yield "B"
    yield "C"

def main_generator():
    """A generator that uses yield from"""
    yield "Start"
    yield from sub_generator()  # Delegate to sub-generator
    yield "End"

print("   Using yield from:")
for value in main_generator():
    print(f"      {value}")

print("\n   yield from makes it easy to chain generators!")


# ============================================================
# PIPELINE WITH GENERATORS
# ============================================================

print("\n5. PIPELINE WITH GENERATORS")

def read_numbers(n):
    """Generate numbers from 0 to n-1"""
    for i in range(n):
        yield i

def square_numbers(nums):
    """Square each number"""
    for num in nums:
        yield num ** 2

def filter_even(nums):
    """Filter even numbers"""
    for num in nums:
        if num % 2 == 0:
            yield num

print("   Pipeline: filter_even(square_numbers(read_numbers(10)))")
pipeline = filter_even(square_numbers(read_numbers(10)))
for value in pipeline:
    print(f"      {value}")

Advanced generator features key points:

  • send() — sends values into the generator
  • throw() — raises exceptions inside the generator
  • close() — stops the generator
  • yield from — delegates to another generator
  • Pipelines — chain generators together for data processing

Quick Check: What does yield from do? (Answer: It delegates to another generator, yielding all its values)

Real-World Example

5

Building a Log File Processor

# Real-World Example: Log File Processor

import re
from datetime import datetime
import random

print("=" * 60)
print("LOG FILE PROCESSOR — GENERATORS IN ACTION")
print("=" * 60)

# ============================================================
# LOG GENERATOR — Simulates reading a log file
# ============================================================

def generate_log_entries(count=20):
    """Generator that produces simulated log entries"""
    
    log_levels = ["INFO", "WARNING", "ERROR", "DEBUG"]
    messages = [
        "User logged in",
        "User logged out",
        "File uploaded",
        "File downloaded",
        "Database connection established",
        "API request received",
        "API response sent",
        "Cache updated",
        "Memory usage high",
        "Request timeout",
        "User authentication failed",
        "System starting up",
        "System shutting down"
    ]
    
    for i in range(count):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        level = random.choice(log_levels)
        msg = random.choice(messages)
        
        # Add some correlation
        if level == "ERROR":
            msg = f"ERROR: {msg} - Code: {random.randint(100, 500)}"
        elif level == "WARNING":
            msg = f"WARNING: {msg} - Threshold: {random.randint(80, 99)}%"
        
        yield f"[{timestamp}] {level}: {msg}"


# ============================================================
# LOG PROCESSOR — Using generators for processing
# ============================================================

def parse_log_entries(entries):
    """Parse log entries and extract structured data"""
    pattern = r'\[(.*?)\] (\w+): (.*)'
    
    for entry in entries:
        match = re.match(pattern, entry)
        if match:
            timestamp, level, message = match.groups()
            yield {
                "timestamp": timestamp,
                "level": level,
                "message": message,
                "raw": entry
            }


def filter_by_level(parsed_entries, level_filter):
    """Filter entries by log level"""
    for entry in parsed_entries:
        if entry["level"] == level_filter:
            yield entry


def extract_errors(parsed_entries):
    """Extract error codes from messages"""
    for entry in parsed_entries:
        if entry["level"] == "ERROR":
            # Extract error code
            error_match = re.search(r'Code: (\d+)', entry["message"])
            if error_match:
                error_code = int(error_match.group(1))
                entry["error_code"] = error_code
                yield entry


def log_summary(parsed_entries):
    """Generate summary statistics from log entries"""
    stats = {}
    
    for entry in parsed_entries:
        level = entry["level"]
        stats[level] = stats.get(level, 0) + 1
    
    # Also count errors by code
    error_codes = {}
    for entry in parsed_entries:
        if entry["level"] == "ERROR" and "error_code" in entry:
            code = entry["error_code"]
            error_codes[code] = error_codes.get(code, 0) + 1
    
    stats["error_codes"] = error_codes
    return stats


# ============================================================
# DEMONSTRATION
# ============================================================

print("\n1. GENERATING LOG ENTRIES")
log_entries = generate_log_entries(15)
print("   Generated 15 log entries")

print("\n2. PARSING LOG ENTRIES")
parsed = parse_log_entries(log_entries)
print("   First 3 parsed entries:")
for i, entry in enumerate(parsed):
    if i >= 3:
        break
    print(f"      [{entry['timestamp']}] {entry['level']}: {entry['message'][:30]}...")

print("\n3. FILTERING BY LEVEL")
# Need to recreate the generator (it's exhausted)
parsed = parse_log_entries(generate_log_entries(15))
warnings = filter_by_level(parsed, "WARNING")
print("   Warning entries:")
for warn in warnings:
    print(f"      {warn['message']}")

print("\n4. EXTRACTING ERROR CODES")
parsed = parse_log_entries(generate_log_entries(20))
errors = extract_errors(parsed)
print("   Errors with codes:")
for err in errors:
    print(f"      Code {err.get('error_code', 'N/A')}: {err['message']}")

print("\n5. GENERATING SUMMARY")
parsed = parse_log_entries(generate_log_entries(15))
summary = log_summary(parsed)
print(f"   Summary: {summary}")


# ============================================================
# PIPELINE — Chaining Generators Together
# ============================================================

print("\n6. PIPELINE — Chaining Generators")

def log_pipeline(entry_count):
    """Complete log processing pipeline"""
    return extract_errors(
        filter_by_level(
            parse_log_entries(
                generate_log_entries(entry_count)
            ),
            "ERROR"
        )
    )

print("   Pipeline: generate → parse → filter → extract_errors")
pipeline = log_pipeline(15)
for error in pipeline:
    print(f"      {error}")

print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("✅ Generators are perfect for processing streams of data")
print("✅ They're memory efficient — only one entry at a time")
print("✅ Pipelines of generators create clean, reusable code")
print("✅ Each step in the pipeline is independent and testable")

Real-world example key points:

  • generate_log_entries — simulates reading log data
  • parse_log_entries — parses and structures the data
  • filter_by_level — filters entries by log level
  • Pipeline — chains generators together for processing
  • Memory efficient — only one entry is processed at a time

Quick Check: Why are generators good for log processing? (Answer: They process one entry at a time, which is memory efficient for large log files)

Best Practices

6

Using Generators Effectively

# Best Practices for Generators

print("=" * 60)
print("BEST PRACTICES FOR GENERATORS")
print("=" * 60)

# ============================================================
# 1. USE GENERATORS FOR LARGE DATASETS
# ============================================================

print("\n1. USE GENERATORS FOR LARGE DATASETS")

# ✅ DO: Use a generator for large datasets
def read_large_file(file_path):
    """Read a large file line by line (memory efficient)"""
    with open(file_path, 'r') as f:
        for line in f:
            yield line.strip()

# ❌ DON'T: Read the entire file into memory
# def read_large_file_bad(file_path):
#     with open(file_path, 'r') as f:
#         return f.readlines()  # Memory intensive for large files

print("   ✅ Generator reads one line at a time")
print("   ❌ Reading all lines at once is memory intensive")


# ============================================================
# 2. USE GENERATOR EXPRESSIONS FOR SIMPLE CASES
# ============================================================

print("\n2. USE GENERATOR EXPRESSIONS FOR SIMPLE CASES")

# ✅ DO: Use generator expressions
numbers = range(1000000)
squares_gen = (x ** 2 for x in numbers)

# ✅ DO: Use generator expressions with functions
total = sum(x ** 2 for x in range(1000000))
print(f"   Sum of squares: {total}")

# ❌ DON'T: Create a list for large datasets
# squares_list = [x ** 2 for x in range(1000000)]  # Memory intensive


# ============================================================
# 3. USE yield from FOR DELEGATION
# ============================================================

print("\n3. USE yield from FOR DELEGATION")

# ✅ DO: Use yield from to delegate
def concatenate(generators):
    for gen in generators:
        yield from gen  # Clean delegation

# ❌ DON'T: Manual delegation
def concatenate_bad(generators):
    for gen in generators:
        for value in gen:  # More code, less clean
            yield value

print("   ✅ yield from is cleaner and more efficient")


# ============================================================
# 4. HANDLE GENERATOR EXHAUSTION
# ============================================================

print("\n4. HANDLE GENERATOR EXHAUSTION")

# ✅ DO: Be aware that generators are single-use
def gen_example():
    yield 1
    yield 2
    yield 3

g = gen_example()
print("   First iteration:")
for x in g:
    print(f"      {x}")

print("   Second iteration (empty):")
for x in g:
    print(f"      {x}")  # Nothing printed

# ✅ DO: Create a new generator if you need to iterate again
g2 = gen_example()
print("   New generator (works again):")
for x in g2:
    print(f"      {x}")


# ============================================================
# 5. USE GENERATORS FOR PIPELINES
# ============================================================

print("\n5. USE GENERATORS FOR PIPELINES")

def source():
    for i in range(5):
        yield i

def transform(data):
    for x in data:
        yield x * 2

def filter_data(data):
    for x in data:
        if x > 5:
            yield x

# Build a pipeline
pipeline = filter_data(transform(source()))
print("   Pipeline output:")
for value in pipeline:
    print(f"      {value}")


# ============================================================
# 6. SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
┌─────────────────────────────┬─────────────────────────────────────────────┐
│ PRACTICE                    │ WHY IT MATTERS                             │
├─────────────────────────────┼─────────────────────────────────────────────┤
│ Use for large datasets      │ Memory efficient                           │
│                             │                                             │
│ Use generator expressions   │ Clean, concise, and lazy                   │
│ for simple cases            │                                             │
│                             │                                             │
│ Use yield from              │ Clean delegation to other generators       │
│                             │                                             │
│ Be aware of single-use      │ Avoid bugs with exhausted generators       │
│                             │                                             │
│ Use generators for          │ Clean, reusable data processing            │
│ pipelines                   │                                             │
└─────────────────────────────┴─────────────────────────────────────────────┘

📌 REMEMBER:
   • Generators are memory efficient
   • They're single-use — create new ones if needed
   • They're perfect for data pipelines
   • yield from makes delegation clean and simple
""")

Best practices summary:

  • Use for large datasets — memory efficient processing
  • Use generator expressions — for simple, lazy operations
  • Use yield from — clean delegation to other generators
  • Be aware of single-use — generators are exhausted after iteration
  • Use for pipelines — chain generators for data processing

Quick Check: What's the main advantage of generators over lists for large datasets? (Answer: Generators are memory efficient — they don't store all values at once)

Try It Yourself

Experiment with generators in the editor below.

Loading Pyodide... 0%
Python Code Editor
==================================================
GENERATORS - PRACTICE
==================================================

1. BASIC GENERATOR
Count to 5:
1
2
3
4
5

2. GENERATOR EXPRESSION
Squares generator:
0
1
4
9
16
25
36
49
64
81

3. GENERATOR WITH send()
Accumulator:
Send 5: 5
Send 10: 15
Send 3: 18

4. PIPELINE WITH GENERATORS
Pipeline: double(filter_even(fibonacci(20)))
Result: [0, 2, 8, 34, 144]
🏆

You've Got It!

You now understand generators in Python. You know how to create generators using yield, use generator expressions, and build data pipelines with generators.

Quick Quiz

Test what you've learned:

1. What keyword is used to create a generator?
2. What's the main advantage of generators over lists?
3. What syntax is used for generator expressions?
4. What does yield from do?
5. Can a generator be reused after it's exhausted?

Frequently Asked Questions

What is a generator in Python? ▼

A generator is a function that uses the yield keyword to produce a sequence of values on demand. Instead of returning all values at once, a generator yields one value at a time, pausing between each yield.

What's the difference between yield and return? ▼

return ends the function and returns a value. yield pauses the function, returns a value, and remembers the state so it can resume later. A function with yield is a generator.

When should I use a generator? ▼

Use generators when working with large datasets, streaming data, infinite sequences, or when you want memory-efficient processing. They're also great for building data processing pipelines.

What's the difference between a generator and a list? ▼

A list stores all values in memory. A generator generates values on demand. Lists are faster for random access and multiple iterations. Generators are memory efficient for large datasets and streaming data.

What does send() do in a generator? ▼

The send() method sends a value into the generator. The value becomes the result of the yield expression inside the generator. This allows two-way communication between the caller and the generator.

Can a generator be infinite? ▼

Yes, generators can be infinite. They will yield values forever until stopped. This is useful for things like infinite counters or streaming data. You need to use a break condition or itertools.islice() to limit the iteration.

Where to Go From Here

Now that you understand generators in Python, check out these related topics:

Iterators

Learn about iterators — the foundation of generators.

Learn More →

Decorators

Learn how decorators work with generators.

Learn More →

Context Managers

Learn about context managers that work with generators.

Learn More →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Iterators Next: Decorators →