Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
171: Generator Functions and yield
I want to show you a piece of code that looks perfectly fine at first glance, but will absolutely tank your production server if you run it against a real-world dataset. I've seen this exact mistake made by junior devs (and even some seniors) more times than I can count.
def get_critical_logs(filename):
critical_errors = []
with open(filename, 'r') as f:
for line in f:
if "CRITICAL" in line:
critical_errors.append(line)
return critical_errors
# Imagine 'huge_system.log' is 15GB
logs = get_critical_logs("huge_system.log")
for log in logs:
print(log)
The Memory Wall
The logic here is sound, right? We open a file, filter for the word "CRITICAL", and return a list of those lines. But here is the problem: the critical_errors list lives in your RAM. If your log file is 15GB and 2GB of that consists of critical errors, Python will try to allocate 2GB of memory to hold that list before it ever reaches the for log in logs loop.
If your server only has 1GB of free RAM, the OS is going to kill your process with a MemoryError. You're trying to swallow the entire ocean before you've even taken a single sip.
Swapping Return for Yield
This is where yield comes in. When you replace return with yield, you aren't writing a standard function anymore—you're creating a generator function. A generator doesn't compute the entire list and hand it over; instead, it pauses its execution and hands back one value at a time, only when requested.
def get_critical_logs(filename):
with open(filename, 'r') as f:
for line in f:
if "CRITICAL" in line:
yield line # This is the magic part
# Now, 'logs' isn't a list; it's a generator object
logs = get_critical_logs("huge_system.log")
for log in logs:
print(log)
Notice that I removed the critical_errors list entirely. Now, the function finds one critical line, yields it to the loop, and then just... sits there. It stays paused exactly where it left off. When the loop asks for the next item, the function wakes up, finds the next match, and yields that. Your memory usage stays flat, regardless of whether the file is 10MB or 10TB.
The Trade-off of Lazy Evaluation
We call this "lazy evaluation" because Python is being lazy—it refuses to do any work until you absolutely force it to by iterating over it. This is incredibly powerful, but there is one big catch you need to remember: generators are one-time-use.
If you try to loop through logs a second time, it will be empty. The "cursor" has already reached the end of the file and the generator is exhausted. If you need the data multiple times, you'll either need to call the generator function again or actually convert it to a list using list(logs)—though doing the latter brings us right back to the memory problem we just solved.
return: "Here is the entire finished product. I'm done now."yield: "Here is one piece. I'll wait right here until you need the next one."
📋 Practical Task
Building a Large-Scale CSV Row Filter
You are tasked with processing a massive CSV file containing user transactions. The file is too large to fit in memory, and you only care about transactions that are over $1,000.00.
Your Goal: Create a generator function called filter_high_value_transactions(data_source). Instead of a real file, the data_source will be a list of strings (simulating lines in a file) to make the exercise easier to run.
Requirements:
- The function must
yieldonly the lines that contain a value greater than 1000. - The lines are formatted as
"UserID,Amount"(e.g.,"user_1,150.00"). - Do not use a list to collect the results inside the function.
# Test data
mock_csv_data = [
"user_1,150.00",
"user_2,1200.50",
"user_3,45.00",
"user_4,3000.00",
"user_5,999.99"
]
# Your generator function here
def filter_high_value_transactions(data_source):
# TODO: Implement the generator
pass
# Testing the generator
high_values = filter_high_value_transactions(mock_csv_data)
for transaction in high_values:
print(f"High value found: {transaction}")
There are no comments for now.