Practice problem
Long Words Per Line
HardSolve in Python · graded against 2 sample tests and a hidden test set in your browser · Published
The Bug Report
The program reads a header line giving how many word lines follow and a minimum word length, then for each
word line prints that line's words of at least that length, in their original order, separated by single
spaces. A line with no long-enough word prints none. It runs, it never raises an error, and the first
line of output is always correct.
Input that shows the problem
2 4
open source tools go
tiny code ideaWhat the program prints
open source tools
open source tools tiny code ideaWhat it should print
open source tools
tiny code ideaThe second line of output contains every word from the first line as well as its own. With three input lines the third output line is longer still. The output only ever grows, and no word is ever wrong on its own — every word printed really is long enough, it just belongs to an earlier line.
Input Format
The input has t + 1 lines:
- Line 1: two integers separated by a single space — t, the number of word lines that follow, and k, the minimum length.
- The next t lines: one line of words each, lowercase letters only, separated by single spaces.
Output Format
Print t lines, one per input word line in order. Each line holds that line's words of length k or more,
in the order they appeared, separated by single spaces. If a line has no such word, print the lowercase
word none on that line instead.
How to Debug It
Reach for the smallest input that shows the growth, and make it as boring as possible so nothing
distracts you. Two lines, and make them identical: a header of 2 1, then x and x again. Correct
output is x twice. If you get x and then x x, you have learned the important thing already — the two
calls are not independent, and the second one begins with something the first one left behind. Note also
what this rules out: the words are read correctly, the length test is correct, and the printing is
correct, because every word printed does satisfy the rule.
Next, locate where that leftover lives, and resist the assumption that it must be a global. Print kept
as the very first statement in the function, before the loop, and run a two-line input. The first call
will report an empty list. The second call will not. That is the fact to explain: a local parameter,
holding data from a previous call, before this call has done anything at all.
Work out which piece of the function could possibly survive between calls. The words are freshly read in
main each time. The minimum is an integer, passed in each time. The only remaining candidate is the
third parameter, which the caller never passes, so its value comes from the default in the def line. Ask
a question you can answer with two lines in a shell: when is a default value in a Python function
definition actually evaluated — every time the function is called, or once when the def runs? Print
id(kept) on both calls if you want to see the answer directly rather than take anyone's word for it.
Change one thing in light of that answer, then re-run the identical-lines input, the failing sample, the
second sample, and a case where a middle line has no long-enough word at all — that last one matters,
because a shared list also breaks none, printing stale words instead of none.
What Was Wrong
The function's third parameter had a mutable default:
def long_words(words, minimum, kept=[]):Python evaluates default argument values once, at the moment the def statement runs — not on each
call. So that [] creates exactly one list, at the moment that def line runs, and it is attached to the
function object itself. Every call that does not pass kept explicitly receives that same list, not a fresh empty one.
Because the body then calls kept.append(word), every call mutates the one shared list. The first call
appends open, source, tools; the second call starts from that list and appends tiny, code,
idea on top; and the function returns the same growing list every time. This is why the output only ever
accumulates, why the first line is always right, and why nothing errors — the code is doing precisely what
it says, just once for the program rather than once per call.
It also quietly breaks the none case. A line with no long-enough word appends nothing, but the shared
list is not empty, so found is truthy and the earlier lines' words are printed instead of none.
The fix is the standard Python idiom for an optional mutable parameter: default to the immutable sentinel
None, and create the real value inside the body, where it is created once per call.
def long_words(words, minimum, kept=None):
if kept is None:
kept = []None is safe as a default precisely because it cannot be mutated, so sharing it between calls means
nothing. The list, which can be mutated, is now built inside the function, so each call gets its own.
The rule to carry away: a default argument value is fine when it cannot change — a number, a string,
True, None, a tuple — and a trap when it can, which covers lists, dicts and sets. If you want a
default that is a fresh empty container, None plus two lines in the body is how you get it.
Common Mistakes
- Reassigning the parameter with
kept = kept or []. It looks tidier and usually behaves, but it also replaces any empty list a caller deliberately passed in, because an empty list is falsy.if kept is Nonetests for the thing you actually mean and leaves a caller's empty list alone. - "Fixing" it by clearing the list at the top of the function with
kept.clear(). That does make each call start empty, but it now silently wipes whatever a caller passed in, which defeats the point of having the parameter. It also leaves the shared-list surprise in place for the next person to read thedefline. - Assuming the leak is a global variable and searching for one. There is no global here, and looking
for one is how this bug survives code review. The shared state is hidden in the function's signature,
which is why "print the parameter before the body does anything" finds it and reading
maindoes not. - Believing a fresh run would show it. Each run of the program starts a new process, so the shared list starts empty and the first output line is correct every time. The accumulation only appears within a single run across multiple calls, which is exactly why a one-line input looks like proof that the program works.
Sample tests
Sample 1
Input
2 4 open source tools go tiny code idea
Expected output
open source tools tiny code idea
With k = 4, the first line keeps open, source and tools and drops go; the second line keeps all three of its words, since each is exactly four letters long.
Sample 2
Input
1 5 short words only here
Expected output
short words
Only short and words reach five letters. There is a single line here, and a single line is the one case the broken program handles correctly.
Your solution
Run tries the first sample. Submit grades against every sample and the hidden tests. Your code is saved in this browser as you go.
Press Esc then Tab to move keyboard focus out of the code editor.
Output will appear here after you run your code.