TL;DR: Separate the pure counting core (text to a Counts dataclass) from the thin I/O shell that reads files and formats output. The structure is the answer: it absorbs new flags, stdin, and multiple files without a rewrite.
How to approach it On a practical screen (Anthropic explicitly says "not LeetCode"), this question is about structure under extension. The interviewer will add flags, then multiple files, then maybe grep-like filtering. So separate three concerns from the start and say so: counting (pure function on text), option handling (which counts to show), and I/O (files/stdin). Pure core + thin shell is the pattern every later multi-part question rewards.
A strong answer Start with the pure core and tests, then wrap it:
from dataclasses import dataclass
@dataclass
class Counts:
lines: int
words: int
chars: int
def count_text(text: str) -> Counts:
return Counts(
lines=text.count("\n"), # wc semantics: newline count
words=len(text.split()), # any-whitespace runs
chars=len(text),
)
Narrate the two semantic decisions; they're the real content. wc counts newlines, so a file without a trailing newline has its last line uncounted; you flag the discrepancy and pick a convention out loud. And split() with no args handles tabs/multiple spaces correctly where split(" ") would not.
def test_count_text():
c = count_text("hello world\nsecond line\n")
assert (c.lines, c.words, c.chars) == (2, 4, 24)
assert count_text("").words == 0 # empty input
assert count_text("no-newline").lines == 0 # the convention we chose
assert count_text("a\t b c\n").words == 3 # whitespace runs
Then the thin shell, where flags select which fields to print, defaulting to all, mirroring real wc:
import sys
def run(paths: list[str], show: set[str]) -> str:
out = []
for path in paths:
try:
text = open(path, encoding="utf-8").read()
except OSError as e:
out.append(f"wc-lite: {path}: {e.strerror}")
continue # keep going, like real wc
c = count_text(text)
fields = [str(getattr(c, f)) for f in ("lines", "words", "chars") if f in show]
out.append("\t".join(fields + [path]))
return "\n".join(out)
A missing file produces an error line and processing continues: small touch, strong "I've shipped CLIs" signal.
| Extension they will ask for | What your structure must already allow |
|---|---|
A new flag, say -L for longest line | Adding one counter without touching the others |
| Reading from stdin as well as files | The counting logic never knowing where bytes came from |
| Multiple files with a total line | Counting returning a value rather than printing |
| A different definition of a word | The tokenizer being one replaceable function |
| Enormous files | Streaming line by line, never read() |
This question is not about counting. It is about whether your first version has a shape that survives the second requirement, which is why the grader adds one the moment you finish.
The multiple-files-with-totals extension is worth showing at full size, because "the structure absorbs it" is a claim and the diff is the proof. The entire change is four lines on the dataclass (written and executed: two files counting 1/2/12 and 2/4/19 sum to 3/6/31):
def __add__(self, other: "Counts") -> "Counts":
return Counts(self.lines + other.lines,
self.words + other.words,
self.chars + other.chars)
and one line in the shell: accumulate total = total + c in the loop, emit a total row after it. Nothing in count_text changed, nothing in flag handling changed, and the reason is visible in hindsight: the core returns a value, and values compose. Compare the counterfactual: if count_text printed its results, the totals extension would mean threading an accumulator through print statements, and the grader would watch the rewrite eat your remaining minutes. This tiny before-and-after is the entire pure-core-thin-shell argument compressed into one extension, and narrating it in those terms is worth more than the four lines themselves.
What interviewers probe next The extensions are the interview. (1) -l/-w/-c flags: already handled by show; you anticipated the API, which is the point. (2) Multiple files + totals row: sum Counts objects; adding __add__ to the dataclass keeps it clean. (3) grep-lite, print lines matching a pattern: the I/O shell is reused; only the core changes, which is exactly the refactorability being scored. (4) 2 GB file? Stream line-by-line (for line in f) instead of read(); the Counts accumulation already works incrementally. (5) Encoding errors? errors="replace" vs failing loudly: discuss, pick, justify.
Common mistakes One 40-line main() mixing parsing, counting, and printing: works for L1, collapses at the first extension, and the rewrite eats your remaining time. Reading whole files when asked about large inputs. Silently choosing word/line semantics instead of stating the convention. No tests until prompted: on Anthropic-style screens, tests-unprompted is close to a hard requirement. And clever one-liners that resist modification: this format scores boring, extendable code.
