# Everything Said Twice

> Every word leaves a mark. Count who keeps coming back.

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

Domain: Python · Difficulty: easy · Seniority: L3

## Problem

Given a string of words separated by whitespace, return a dict mapping each distinct word to the number of times it appears.

## Worked solution and explanation

### What this is really testing

Strip the costume and this is a tokenize-and-tally: chop a string into words, count how many times each shows up. Everyone gets the counting part right; almost nobody trips on the dict accumulation. The thing that actually separates people is one keystroke: whether you call text.split() with no argument or text.split(' ') with a literal space. They look identical on the happy path 'a b c' and diverge the instant the input has a double space, a tab, or a leading blank. Pick the wrong one and you silently seed your dict with an empty-string key that carries a real count, and your output is wrong in a way that passes casual eyeballing.

> **The one insight**
>
> str.split() with no argument is a completely different function from str.split(' '). No-arg split treats any run of whitespace as a single delimiter AND trims the ends, so '  the  the cat '.split() is exactly ['the', 'the', 'cat']. That single behavior handles every whitespace trap in the problem for free. Reach for it and the rest is a one-liner.

### How you get there

#### Step 1: Split on whitespace, not on a space character

The spec says 'separated by whitespace' and that phrasing is the tell. No-arg split() collapses runs of spaces, tabs, and newlines, and strips leading/trailing whitespace, so you never manufacture empty tokens. The moment you hardcode split(' ') you are counting on the input being perfectly single-spaced, which real text never is.

#### Step 2: Tally in one pass

Feed the token list straight into collections.Counter. It walks the tokens once, O(n), and increments in C. You could hand-roll d[w] = d.get(w, 0) + 1 and it would be correct, but Counter is the idiom a reviewer expects here and it comes with .most_common() for the inevitable follow-up.

#### Step 3: Hand back a plain dict

The signature promises -> dict. Counter IS a dict subclass and compares equal, so tests usually pass either way, but wrapping in dict() matches the contract exactly and won't surprise a caller that inspects the concrete type. Empty input needs no special case: ''.split() is [], Counter([]) is empty, dict() is {}.

**Counter over whitespace-split tokens**

```python
from collections import Counter

def word_counts(text: str) -> dict:
    return dict(Counter(text.split()))
```

*One pass to split, one pass to count.*

**split(' ') — the trap**

'  the  the cat '.split(' ') -> ['', '', 'the', '', 'the', 'cat', ''] . Counter now has a '' key with count 3 and 'the' count 2. The output silently carries a junk key and passes a quick glance.

**split() — correct**

'  the  the cat '.split() -> ['the', 'the', 'cat'] . Counter is {'the': 2, 'cat': 1} . Runs collapse, ends trim, no empty tokens, ever.

> **Common pitfall**
>
> Two ways candidates lose this. First: split(' ') with an explicit space, which injects empty-string keys on any padded or multi-spaced input like '  click view  '. Second: assuming case folds — 'Go go GO' is three distinct words here, not one. Do NOT sneak in a .lower() unless the spec asks; the hidden tests expect GO, Go, and go as separate keys.

> **Interviewers watch for**
>
> The tell of seniority is reaching for Counter without hesitation, split() with no arg, and naming the empty-input case out loud before being asked. Bonus signal: mentioning .most_common(k) exists the second the interviewer starts to say 'now give me the top…'.

> **Why it stays cheap**
>
> O(n) in the length of text: split scans the string once, Counter scans the tokens once. Space is O(u) for u distinct words. Counter's increment loop runs in C, so it edges out a Python-level d.get() loop by a comfortable margin — but the real win is that there's no second pass and no sort unless a follow-up demands ranking.

> **In production**
>
> This exact shape is everywhere: event-tag frequency, log-level counts, word clouds, n-gram tallies. The whitespace trap is the same one that corrupts real analytics when someone splits log lines on ' ' and a tab-delimited field quietly produces phantom empty tokens in the histogram.

## Common follow-up questions

- What changes if words should be counted case-insensitively? _(Counter(text.lower().split()) — but note .lower() allocates a full copy, O(n) extra memory, and check whether the spec actually wants case folded.)_
- How would you return the top 3 most common words? _(Counter(...).most_common(3) gives (word, count) tuples in descending order; be ready to discuss that ties break on insertion order.)_
- How would you handle a 10 GB file that doesn't fit in memory? _(Stream line by line, split each line, Counter.update(tokens) in place; the Counter stays bounded by the vocabulary size, not the file size.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/everything_said_twice)
- [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.