Learning Goals
3 minBy the end of this lesson you can:
- Write a generator with no stop condition — yields values forever.
- Use
itertools.isliceto take just the first N items. - Use
breakinside a for-loop to stop early. - Avoid the classic infinite-list trap:
list(infinite_generator)= freeze.
Warm-Up · Forever-Counter
5 mindef integers(): """Yields 1, 2, 3, 4, ... forever.""" n = 1 while True: yield n n += 1 # DO NOT do list(integers()) — that's a forever-loop g = integers() print(next(g)) # 1 print(next(g)) # 2 print(next(g)) # 3 # ... we could go forever, but we won't.
An infinite generator. The function never returns; the while True sees to that. Each next() runs one cycle.
Generators don't need to end. itertools.islice lets you take any finite slice without ever building the full sequence. That's how infinite-yet-memory-safe works.
New Concept · Three Infinite Patterns
14 minPattern 1 · Infinite arithmetic
def naturals(): n = 1 while True: yield n n += 1 def evens(): n = 2 while True: yield n n += 2
Pattern 2 · Infinite Fibonacci
def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b
Three lines. Yields the full infinite sequence — 0, 1, 1, 2, 3, 5, 8, 13, ...
Pattern 3 · Infinite primes
One naïve, beautiful version:
def primes(): n = 2 while True: is_prime = all(n % i != 0 for i in range(2, int(n ** 0.5) + 1)) if is_prime: yield n n += 1
Generator yielding primes 2, 3, 5, 7, 11, 13, .... It's slow at scale, but conceptually clean.
The slicing problem
Calling list(fibonacci()) would freeze. You need to take a finite slice. Three ways:
# Way 1 — manual loop with break def first_n(gen, n): out = [] for x in gen: if len(out) >= n: break out.append(x) return out print(first_n(fibonacci(), 10)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # Way 2 — itertools.islice (the standard answer) from itertools import islice print(list(islice(fibonacci(), 10))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # Way 3 — enumerate + break for i, x in enumerate(fibonacci()): if i >= 10: break print(x)
islice from the itertools module is the standard way. islice(iterable, n) = "take first n". islice(iterable, start, stop) = "take from start to stop".
islice doesn't materialise
islice itself is an iterator — it yields the values one at a time from the source. No intermediate storage.
from itertools import islice big = islice(fibonacci(), 100_000_000) # 100 million fibs total = sum(big) # streams them; no memory spike print(total)
You'd run out of patience before memory.
Other useful itertools
count(start, step) 1, 2, 3, ... or start, start+step, start+2*step, ... cycle(iterable) a, b, c, a, b, c, a, ... forever repeat(x) x, x, x, x, ... forever islice(it, n) take first n chain(a, b, c) concatenate iterables
from itertools import count, cycle, islice # Power of 10 with count list(islice((10 ** i for i in count()), 5)) # [1, 10, 100, 1000, 10000] # Round-robin with cycle team_letters = islice(cycle("ABC"), 7) print(list(team_letters)) # ['A', 'B', 'C', 'A', 'B', 'C', 'A']
The infinite-list trap
This is the bug that catches everyone once:
# ❌ Runs forever and eats all your memory all_fibs = list(fibonacci()) # ❌ Same — sorted has to read everything first sorted_fibs = sorted(fibonacci()) # ❌ Same — sum needs every value total = sum(fibonacci())
For an infinite generator, anything that needs to consume everything is a forever-loop. Always slice first.
Worked Example · Primes Up to a Limit
12 minSave as primes_take.py:
# primes_take.py — slicing an infinite stream from itertools import islice def primes(): """Infinite generator of primes.""" n = 2 while True: if all(n % i != 0 for i in range(2, int(n ** 0.5) + 1)): yield n n += 1 def take(it, n): """Take the first n items as a list.""" return list(islice(it, n)) def take_while(it, pred): """Take items while pred is True. From itertools.takewhile.""" for x in it: if not pred(x): return yield x # 1 — first 10 primes print("First 10 primes:") print(take(primes(), 10)) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] # 2 — primes less than 50 print("\nPrimes < 50:") print(list(take_while(primes(), lambda p: p < 50))) # 3 — sum of first 100 primes print(f"\nSum of first 100 primes: {sum(islice(primes(), 100))}") # 4 — every 10th prime, first 5 of those def every_nth(it, n): """Yield every nth item starting from the 0th.""" for i, x in enumerate(it): if i % n == 0: yield x print(f"\nEvery 10th prime, first 5: {list(islice(every_nth(primes(), 10), 5))}")
Output
First 10 primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] Primes < 50: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] Sum of first 100 primes: 24133 Every 10th prime, first 5: [2, 31, 73, 127, 179]
Read the diff
An infinite source. Four different finite operations on it — first N, while a condition, sum of first N, every Nth. Each combines the generator with islice or another generator helper. The source never knows or cares how many primes are wanted. Lazy.
Try It Yourself
13 minUsing yesterday's fibonacci generator + islice, print the first 20 Fibonacci numbers as a list.
Hint
from itertools import islice def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b print(list(islice(fibonacci(), 20)))
Write a generator powers_of_2() that yields 1, 2, 4, 8, 16, ... forever. Take the first 15.
Hint
def powers_of_2(): n = 1 while True: yield n n *= 2 from itertools import islice print(list(islice(powers_of_2(), 15))) # [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384]
Pipe fibonacci through a generator filter that keeps only evens, then take the first 5.
Hint
def evens_only(gen): for x in gen: if x % 2 == 0: yield x first_5_even_fibs = list(islice(evens_only(fibonacci()), 5)) print(first_5_even_fibs) # [0, 2, 8, 34, 144]
Two generators composed. The infinite source never stops; the filter passes only evens; islice takes 5. Stream all the way down.
Mini-Challenge · The Token Bucket
8 minBuild a generator token_bucket(rate_per_min) that yields the current time in seconds-since-epoch every 60/rate seconds. Use time.sleep between yields. Test by taking 5 tokens at rate 60/min — should be about 5 seconds total.
Show one possible solution
# token_bucket.py — a "rate-limited tick" generator import time from itertools import islice def token_bucket(rate_per_min): interval = 60 / rate_per_min while True: yield time.time() time.sleep(interval) start = time.time() for ts in islice(token_bucket(rate_per_min=60), 5): print(f" tick at +{ts - start:.1f}s") print(f"\nTotal: {time.time() - start:.1f}s")
Non-negotiables: infinite generator that yields and sleeps, islice taking exactly 5 tokens. Each tick prints ~1 second apart. This is the shape behind every rate-limiter, every metronome, every cron-like scheduler.
Recap
3 minA generator with while True: never stops. Take a finite slice with itertools.islice(gen, n). The whole iterator can be infinite as long as you never call list, sum, sorted or anything that needs the whole sequence. itertools ships with count, cycle, repeat, chain, and islice — pre-built infinite helpers. Memory-safe streaming is the killer feature.
Vocabulary Card
- infinite generator
- A generator with no exit.
while True: yield .... - itertools.islice
- Slice any iterable lazily.
islice(it, n)= first n.islice(it, a, b)= a..b-1. - itertools.count
- Infinite arithmetic sequence:
count()= 0,1,2,...count(10, 2)= 10,12,14,... - itertools.cycle
- Repeats an iterable forever.
- infinite-list trap
list(infinite_gen)freezes the program. Always slice first.
Homework
4 minBuild infinite_helpers.py with three infinite generators and one consumer:
triangular_numbers()— yields 1, 3, 6, 10, 15, ... (n×(n+1)/2).letters()— yields 'a', 'b', ..., 'z', 'a', 'b', ... forever (cycle).random_walk()— yields a running sum, each step addsrandom.choice([-1, +1]).- Use islice to print the first 10 triangular numbers, the first 30 letters, and the position of the random walk after 100 steps.
Sample · infinite_helpers.py
from itertools import islice import random def triangular_numbers(): n = 1 total = 0 while True: total += n yield total n += 1 def letters(): while True: for c in "abcdefghijklmnopqrstuvwxyz": yield c def random_walk(): pos = 0 while True: yield pos pos += random.choice([-1, 1]) print(list(islice(triangular_numbers(), 10))) # [1, 3, 6, 10, 15, 21, 28, 36, 45, 55] print("".join(islice(letters(), 30))) # 'abcdefghijklmnopqrstuvwxyzabcd' # Position after 100 random walk steps walk_iter = random_walk() last = None for x in islice(walk_iter, 101): # 0 + 100 steps last = x print(f"After 100 steps: {last}")
Non-negotiables: three infinite generators, islice for taking finite slices, the random walk consumed exactly 101 times (start + 100 steps). All memory-safe.