Blog
Part of 30 Days Back to Basics· 5/5

Day 4: A String Bug Passed Its Own Test For the Wrong Reason

7 July 20263 min readpythonlerning-in-publicbeginnerdebugging
Day 4: A String Bug Passed Its Own Test For the Wrong Reason

Skipped a session for my birthday. Logged it as skipped, not renumbered. The day count moves with sessions, not the calendar, so this is still Day 4.

The bug that stuck with me most today wasn't the one that crashed. It was the one that looked right.

I was writing a palindrome checker (no [::-1], no reversed(), had to actually compare characters). My first working-ish version used in to compare the reversed string against the original: if reverse in s. I ran it against "racecar" and "fish". Got True and False. Looked correct. Moved on.

It wasn't correct. in checks whether one string is a substring of another, and a string is always a substring of itself, no matter what. So "racecar" in "racecar" returns True, but not because they're equal. It's because any string trivially contains itself. The test only exposed itself as wrong when I ran it against something with partial overlap, like "abcabc", where the reversed version ("cbacba") isn't a substring of the original in the way that would actually confirm correctness. Switched to ==, actual value equality, and the false confidence went away.

That's the one I want to remember: a bug that produces the output you expect isn't the same thing as correct code. It's worse, in a way, because there's no error message pointing at it. You have to go looking for a case that breaks your assumption on purpose.

is vs in vs == has been the running theme of the last few days, honestly. Identity, membership, and value equality are three different questions, and I keep reaching for the wrong one: vowel counter, palindrome, and then again today in a bigger project. The resolution time is shrinking each round, which is the only reason I'm not more annoyed about it.

After closing out the string exercises, I rolled straight into something bigger: a contact book. Nested dictionary, name mapping to phone and email, meant to be driven by a command loop instead of hardcoded test data. Four functions (add, lookup, delete, list) all working and tested by hand now.

The delete function taught me something dumb-obvious in hindsight: I assumed setting a contact's value to "" would delete it. It doesn't. It just blanks the value and leaves the key sitting there, empty. Deleting a key entirely needs del. I hadn't needed to reach for that keyword before because I'd never actually tried to make something disappear.

The messiest one was list_contacts. First version only ever returned the first contact, because return was sitting inside the loop, same category of mistake as Day 3's return-inside-loop bug, just wearing a different outfit. Fixed the placement, then immediately overwrote a single results variable every pass through the loop, so by the end it only held the last contact. Tried to fix that with results = results + results, which just doubles whatever's already there, not what I wanted at all. Landed on the actual fix eventually: start with an empty list before the loop, .append() one item each pass.

The return-vs-print bug also isn't dead yet. Hit x = print(...) again today, once in the vowel counter, once in a contact lookup function, and once in a throwaway joke function I wrote mid-session just to mess around (is_forever_love(p1, p2), don't ask). Caught all three faster than the day before, so it's fading, just not gone.

Still not done: the command loop that turns the four contact book functions into something you can actually run and interact with instead of calling by hand. That's tomorrow's problem.

#30DaysBackToBasics

In this series · 30 Days Back to Basics

  1. 01Day 0: 30 Days Back to Basics
  2. 02Day 1: Bricks Before Blueprints
  3. 03Day 2: Nothing Comes Back
  4. 04Day 3: Five Bugs to Reverse a String
  5. 05Day 4: A String Bug Passed Its Own Test For the Wrong Reason