The obvious way to alternate two lists, and what it does when they are not the same length.
a, b = [1, 2, 3], ['x'] [item for pair in zip(a, b) for item in pair] # -> [1, 'x'] the 2 and the 3 never happened from itertools import zip_longest [item for pair in zip_longest(a, b) for item in pair] # -> [1, 'x', 2, None, 3, None] now there are holes # What is actually wanted: [1, 'x', 2, 3]
- Neither zip nor zip_longest is the answer here
zip truncates and zip_longest pads, and 'carry on without the ones that finished' is a third behaviour. zip_longest can be made to work by filtering out a sentinel, but only if you can find a fill value that cannot appear in the data — and None usually can. When neither standard tool matches the rule, write the rule.
- *sequences gathers, so the function shape is free
Declaring *sequences lets the same function serve two lists, five, or none, and the caller can still pass a list of lists with interleave(*groups). Compare that with a version taking a single list of sequences: it is one fewer star at the call site and one more layer of brackets for everybody who has exactly two.
- Index by position, and let short sequences fall out
Walking positions from 0 up to the longest length, and skipping any sequence that has run out, produces the round-robin directly. It reads as the rule written down — everyone gets a turn until they have nothing left — and it needs no sentinel and no bookkeeping about who is still in.
Think in rounds rather than in pairs. Round 0 takes item 0 from everyone who has one, round 1 takes item 1, and so on until nobody has anything at that position.
The number of rounds is the length of the longest sequence. Inside each round, walk the sequences in order and append sequence[index] only when index is still inside that sequence.
max((len(s) for s in sequences), default=0) gives the round count and survives being handed nothing at all — without default it raises ValueError on an empty input. The membership test is just index < len(sequence).
def interleave(*sequences): result = [] rounds = max((len(s) for s in sequences), ____=0) for index in range(____): for sequence in ____: if index < len(sequence): result.append(sequence[____]) return result
def interleave(*sequences):
result = []
rounds = max((len(sequence) for sequence in sequences), default=0)
for index in range(rounds):
for sequence in sequences:
if index < len(sequence):
result.append(sequence[index])
return result
max() over nothing raises ValueError, and 'no sequences' is a legitimate call — so without the default the function has a crash hiding in its most boring input. The keyword exists precisely for reductions over possibly-empty collections, and it is worth reaching for by habit rather than after the traceback.
The alternative implementation keeps a list of live iterators and removes each one as it raises StopIteration — which works, and now the loop is mutating the collection it is walking, which is its own class of bug. Asking 'does this sequence still have an item at this position' each time is a little more work and no state at all.
len() and indexing mean the inputs have to be things you can measure and index — lists, tuples, strings. Hand it a generator and it fails, which is a real limitation given that 'merge these streams fairly' is often asked about streams. The iterator version is the itertools roundrobin recipe, and it is genuinely harder to write correctly.
The itertools documentation ships a roundrobin recipe that does this lazily for any iterables, including infinite ones, and more-itertools packages it as more_itertools.roundrobin along with interleave and interleave_longest. Use those when the inputs are iterators or when you want to stop partway through. Write this version when the inputs are ordinary lists and you want something a reader can follow without knowing the recipe — and remember that fairness by position is only one definition of fair. A playlist that alternates by *duration*, or a queue weighted by priority, is a different function wearing similar clothes.
- Add a weight per sequence, so one of them gets two turns to another’s one, and decide what a weight of zero means.
- Make it work with generators by keeping iterators and calling next() with a sentinel default, and compare how readable the result is.
- Write the inverse — deal(items, n) splitting a list into n round-robin piles — and check that interleaving the piles gives the original back.