-
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
178: The itertools Module: tee, zip_longest, starmap
When you start working with large datasets or streaming APIs in Python, you'll quickly realize that converting everything to a list is a recipe for a MemoryError. I've seen plenty of juniors try to "save" a generator by casting it to a list just so they can iterate over it twice. That works fine for a few hundred items, but when you're dealing with a million-row log file, you're essentially defeating the purpose of using iterators in the first place.
The Memory Trap of Re-iterating
Imagine you have a stream of sensor readings and you want to calculate the difference between each reading and the one that follows it. The naive approach is to pull the whole stream into a list, then use a range-based loop to look at index i and i+1. But if that stream is coming from a socket, you can't just "index" into it.
You might try to create two separate copies of the iterator, but since iterators are stateful, once you advance one, the other doesn't magically stay behind—unless you use itertools.tee. Here is how we handle this without blowing up the RAM:
import itertools
def get_readings():
# Simulating a massive stream of data
for i in range(1000000):
yield i * 1.1
readings = get_readings()
# Instead of list(readings), we split the stream
it1, it2 = itertools.tee(readings, 2)
# Advance the second iterator by one position
next(it2, None)
# Now we can zip them to get (current, next) pairs
for current, following in zip(it1, it2):
# Process the delta...
pass
The trade-off here is subtle. tee doesn't actually copy the underlying data; it keeps a queue of the elements that one iterator has seen but the other hasn't. If it1 and it2 stay close to each other, memory usage remains low. But if you advance it2 a million steps ahead of it1, tee will store those million elements in memory anyway. It's a tool for small offsets, not for creating independent copies of massive datasets.
The Silent Data Loss of Standard Zip
Next, let's talk about alignment. Often, you'll have two streams of data—say, sensor values and corresponding metadata tags—that you expect to be the same length. The standard zip() is great, but it has a dangerous habit: it stops as soon as the shortest iterable is exhausted. In a production environment, this is a silent failure. You lose data and your logs don't tell you why.
If you need to ensure every single data point is processed, even if the metadata stream runs dry, you want itertools.zip_longest. It lets you specify a fillvalue to use when one side finishes early.
from itertools import zip_longest
readings = [22.1, 22.5, 23.0, 21.8]
tags = ["Normal", "Normal"] # Missing tags for the last two
# Naive zip would just give us two pairs and stop.
# zip_longest keeps going.
for reading, tag in zip_longest(readings, tags, fillvalue="Unknown"):
print(f"Reading: {reading}, Status: {tag}")
By using zip_longest, you're making an explicit decision about how to handle missing data. It forces you to acknowledge that your data streams might be asymmetrical, which is almost always the case in the real world.
The Unpacking Overhead of Map
Finally, there's starmap. You're likely familiar with map(), which takes a function and a few iterables. But map expects the function to take arguments as individual parameters, while the data is often already bundled into tuples (especially after a zip or zip_longest operation).
The naive way to handle this is to use a lambda to unpack the tuple, like map(lambda x: my_func(x[0], x[1]), zipped_data). It's clunky, it's slightly slower, and it's visually noisy. starmap solves this by automatically unpacking the tuple into the function arguments for you.
from itertools import starmap
def calculate_variance(val, baseline):
return val - baseline
# Imagine this is the result of our previous zip_longest
data_pairs = [(22.1, 20.0), (22.5, 20.0), (23.0, 20.0)]
# Instead of: map(lambda p: calculate_variance(p[0], p[1]), data_pairs)
variances = starmap(calculate_variance, data_pairs)
print(list(variances))
I prefer starmap because it expresses the intent clearly: "I have a collection of argument tuples, and I want to apply this function to each set." It keeps your logic clean and avoids the overhead of creating a throwaway lambda function for every single call.
📋 Practical Task
Exercise: Telemetry Stream Processor
You are building a tool to analyze a stream of telemetry packets. Each packet consists of a (timestamp, value) tuple. Your goal is to calculate the "rate of change" between consecutive packets and pair that result with a set of "Quality Flags" that might be shorter than the telemetry stream.
Requirements:
- Start with a generator that yields
(timestamp, value)tuples (e.g.,(100, 10.5), (101, 11.2), (102, 11.8), (103, 12.5)). - Use
itertools.teeto create two versions of the generator to calculate the difference in values between consecutive packets. - Write a function
calc_diff(p1, p2)that takes two packets and returns the difference in their values:p2[1] - p1[1]. - Use
itertools.starmapto applycalc_diffto the zipped pairs of packets. - Finally, use
itertools.zip_longestto pair these calculated differences with a list of flags (e.g.,["Valid", "Valid"]). If there are more differences than flags, use"No-Flag"as the fill value.
Expected Output Format:
A list of tuples where each item is (difference, flag).
There are no comments for now.