DEV Community

Jeff Thoensen
Jeff Thoensen

Posted on Originally published at jeffthoensen.com

None Is Not an Error

I have a small CLI tool that looks up an MLB player's season stats by name. The lookup used to fail without ever raising anything: if the MLB API had no player matching a name, search_player_id printed "Player not found." and returned None, and it was on whoever called it to check for that and stop. If that check gets missed anywhere in the chain, None gets passed into the next function that expected a real player ID.

Testing that old version meant capturing whatever the script printed and asserting on the message, then separately checking that the return value was None. Replacing the print-and-return-None pattern with a real exception, PlayerLookupError, meant one assertion covers it: pytest.raises(PlayerLookupError) either happens or it doesn't, with nothing else to check.

The function that decides between pitching and hitting stats used to return (None, None) when a player had a season with no usable stats, the same shape the caller was already checking for a missing player entirely, so both very different problems ended up as the same "something is None, give up" check. Raising PlayerLookupError with an actual message, "No usable stats available for 2025" versus "No player found matching...", means you don't have to guess which one happened.

Running the script with a name as an argument, instead of only through the interactive prompt, meant tests could call the lookup function directly with a name and a mocked response, instead of needing to fake stdin to drive the interactive version. The rewrite, tests included, is on GitHub.

Jeff Thoensen is a Senior QA Engineer building AI-assisted test automation and shipping production code. Find more at jeffthoensen.com.

Top comments (2)

Collapse
 
raknaos profile image
Raknaos • Edited

The (None, None) detail is the real finding here. Two genuinely different situations - no player matched the name, and the player exists but that season has nothing usable - collapsed into the same shape, so every caller had one if x is None: give up branch and no way to know which problem it was handling.

What I'd add: the exception only paid off once the message was distinct too. No usable stats available for 2025 vs No player found matching... means the caller can stay dumb and a human reading a log line can still tell the two apart. If a caller could never act differently on one versus the other, the raise is mostly a structured log line - which is fine, as long as that's the reason you did it rather than "exceptions are better".

The stdin thing is the same bug wearing different clothes. We have a couple of CLI tools in our setup that were only driven through an interactive prompt, so tests faked stdin, and every test was really testing the prompt parser. Accepting the argument positionally made the actual logic reachable for the first time.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.