Skip to content

Practice problem

Long Words Per Line

Hard

Solve 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

Input
2 4
open source tools go
tiny code idea

What the program prints

Output
open source tools
open source tools tiny code idea

What it should print

Output
open source tools
tiny code idea

The 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:

Python
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.

Python
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 None tests 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 the def line.
  • 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 main does 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.

Ctrl/Cmd+Enter to run

Press Esc then Tab to move keyboard focus out of the code editor.

Ready
Output will appear here after you run your code.

More problems

All practice problems →