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
59: Generator Expressions vs Comprehensions
By now, you're probably comfortable with list comprehensions. They're elegant, they're fast, and they're a staple of "Pythonic" code. But there's a twin sibling to the list comprehension called the generator expression. They look almost identical—the only difference is that one uses square brackets [] and the other uses parentheses ()—but they behave fundamentally differently under the hood.
To see why this matters, let's build a small utility to process sensor readings. Imagine we have a stream of temperature data from a factory floor, and we only care about the readings that are over 100 degrees (the "overheat" events).
The instinctive approach (and why it's dangerous)
My first instinct is usually to just throw everything into a list. It's intuitive. I can see the data, I can slice it, and I can print it. Here is how I'd start writing this:
# Simulating a massive stream of 10 million readings
readings = range(10_000_000)
# Find all readings over 100 (just as a dummy example)
overheats = [temp for temp in readings if temp > 100]
print(f"Found {len(overheats)} overheating events.")
Now, if this were a small script, I wouldn't even notice a problem. But in a production environment with truly massive datasets, I've actually crashed a server by doing this. Why? Because that list comprehension creates the entire list of overheating events in memory before it even lets me move to the next line of code. I'm asking Python to allocate enough RAM to hold millions of integers all at once.
Switching to lazy evaluation
This is where I make the pivot to a generator expression. I don't actually need the whole list of overheating events sitting in memory; I just need to be able to iterate through them one by one to process them. I'll change those square brackets to parentheses:
# Now using a generator expression
overheats_gen = (temp for temp in readings if temp > 100)
# I can still loop through it
for event in overheats_gen:
# Process the event here
if event > 1000: # Let's just stop early for the example
break
The magic here is "lazy evaluation." The generator doesn't calculate the filtered list upfront. Instead, it returns a generator object that knows how to produce the next item only when you ask for it. Your memory usage stays flat, regardless of whether you're processing ten items or ten billion.
The "Wait, where's my data?" moment
I'll be honest: every time I start using generators, I make the same mistake. I try to treat the generator like a list. I'll do something like this:
overheats_gen = (temp for temp in readings if temp > 100)
print(overheats_gen[0]) # TypeError: 'generator' object is not subscriptable
I'll get a TypeError because generators don't support indexing. They don't "know" what the first or last element is until they've actually traveled through the sequence to find them. If you absolutely need to access elements by index or need to know the len() of the collection, you're stuck with a list comprehension (or you have to cast the generator to a list using list(overheats_gen), which brings you right back to the memory problem we just solved).
Choosing the right tool for the job
So, how do I decide which one to use? I follow a simple rule of thumb: if I'm going to iterate over the result once (like in a for loop or passing it into a function like sum() or max()), I use a generator expression. If I need to keep the data around to use it multiple times, or if I need to sort it or index it, I use a list comprehension.
📋 Practical Task
Memory-Efficient Log Filtering
You are tasked with analyzing a massive system log. Instead of loading all "ERROR" entries into a list—which would crash the system—you need to create a memory-efficient pipeline.
Write a script that does the following:
- Creates a list of strings called
raw_logscontaining a mix of "INFO", "WARNING", and "ERROR" messages (e.g.,["INFO: System boot", "ERROR: Disk full", "INFO: User login", "ERROR: Network down"]). - Creates a generator expression that filters this list to only include the strings that start with "ERROR".
- Uses a
forloop to iterate through that generator and print each error message in uppercase.
Ensure you use a generator expression () and not a list comprehension [] to ensure the filtering happens lazily.
There are no comments for now.