# Never Walk Alone

> Every step is measured against the one before it.

Canonical URL: <https://datadriven.io/problems/never_walk_alone>

Domain: Python · Difficulty: easy · Seniority: L3

## Problem

Given a text string, split on whitespace and return a list of consecutive 2-word pairs, where each pair is a list [word_i, word_{i+1}]. Return an empty list if there are fewer than 2 words.

## Worked solution and explanation

Strip the costume and this is a width-2 sliding window over a token stream - bigrams, the thing every NLP pipeline builds before it does anything interesting. The window part is trivial; the whole problem lives in two decisions that look cosmetic and aren't. First, how you tokenize: reach for text.split(' ') and a tab or a double space quietly injects empty-string 'words' into your output, so 'sun	moon  stars' comes back with ['', ''] pairs that no grader will forgive. Second, how far you loop: iterate to len(words) instead of len(words) - 1 and the last step reads words[i + 1] off the end - IndexError, on the very input the interviewer hands you to check boundaries.

> **The one move that cracks it**
>
> Call text.split() with NO argument. The bare form collapses arbitrary runs of spaces, tabs, and newlines into single delimiters AND drops leading/trailing whitespace, so you never manufacture an empty token. split(' ') does none of that - it splits on each single space literally. This one choice makes both the empty-input case and the messy-whitespace case fall out for free.

### How a senior reasons through it

#### Step 1: Tokenize with the whitespace-collapsing split

words = text.split() - no argument. I pick this deliberately over split(' ') because the inputs include tabs and double spaces, and the bare split makes runs-of-whitespace a non-issue instead of a bug I have to patch later with a filter.

#### Step 2: Walk adjacent index pairs, not disjoint chunks

range(len(words) - 1) gives me every starting index that has a right-hand neighbor. Each interior word appears in two pairs - as the right of one and the left of the next - because it is a SLIDING window, not two-at-a-time chunking. Getting this wrong drops half the pairs.

#### Step 3: Let the bound handle the short inputs

I don't write a special case for empty or single-word text. When len(words) is 0 or 1, len(words) - 1 is -1 or 0, range() is empty, and the loop never runs - so I return [] automatically. The edge case is absorbed by the loop bound instead of a guard clause I could forget.

**Sliding window of width 2 over tokens**

```python
def sequential_word_pairs(text):
    words = text.split()
    result = []
    for i in range(len(words) - 1):
        pair = [words[i], words[i + 1]]
        result.append(pair)
    return result
```

*Bare split() for tokenizing; range(len(words) - 1) makes the short-input case disappear.*

> **Common pitfall**
>
> Looping with range(len(words)) so the final iteration evaluates words[i + 1] past the end and throws IndexError. The off-by-one is the classic sliding-window slip: the number of bigrams is always one LESS than the number of words, so the bound is len(words) - 1.

> **Interviewers watch for**
>
> Whether you reach for split() or split(' '). The bare call signals you know whitespace is messy in the real world and you've internalized the idiom that handles it; split(' ') plus a later 'if word' filter signals you learned the lesson the hard way and are still patching around it.

**split(' ') - literal, fragile**

'sun	moon  stars'.split(' ') -> ['sun	moon', '', 'stars']. The tab stays glued inside a token and the double space yields a ''. Your pairs now contain garbage, and an empty input gives [''] (length 1, not 0).

**split() - collapsing, robust**

'sun	moon  stars'.split() -> ['sun', 'moon', 'stars']. Tabs and repeated spaces collapse, edges trim, and ''.split() -> [] cleanly. Same line of code, none of the failure modes.

> **Performance insight**
>
> O(n) time and O(n) space for n words: split is one linear pass, the loop is one more, and the result holds n - 1 pairs. At the stated ceiling of ~10,000 words this is a handful of milliseconds - there is no pressure to get clever, so clarity wins outright.

## Common follow-up questions

- How would you generalize this to arbitrary n-grams? _(Parameterize the window: loop range(len(words) - n + 1) and slice words[i:i + n]. Tests whether the off-by-one generalizes.)_
- What if you needed the count of each unique bigram instead of the list? _(Counter keyed by tuple(pair) - lists aren't hashable, so the tuple conversion is the tell.)_
- How would you handle punctuation stuck to words, like 'fox.'? _(Normalize before tokenizing - regex \w+ or str.translate - which shifts the problem from whitespace splitting to real tokenization.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/never_walk_alone)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). DataDriven is the data engineering interview community. Live code execution in SQL, Python, and Spark sandboxes. Every feature is open to every member.