print() shows values the way a person would read them, which is exactly wrong for debugging. The text "12" and the number 12 look identical. A trailing space or a tab is invisible. repr() shows what the value is: quotes around strings, escapes for whitespace.
Before
The classic: a number that came in as text, typed by a user or read from a file.
stock = {"apples": 12}
entered = "12"
print(entered, stock["apples"])
print(entered == stock["apples"])12 12 False
They print the same, yet they aren't equal. Nothing on screen says why.
After
stock = {"apples": 12}
entered = "12"
print(repr(entered), repr(stock["apples"]))'12' 12
The quotes give it away: one is a string. The fix is int(entered), and if that ever fails, the error has its own page: invalid literal for int().
Invisible whitespace
The other half of the problem: text that looks right and isn't.
name = "Ada \t" print(f"[{name}]") print(f"{name!r}")
[Ada ] 'Ada \t'
The first line hides a trailing space and a tab. !r inside an f-string calls repr() for you and names them. When name == "Ada" mysteriously fails, this is usually why, and name.strip() is usually the fix.
Why it works
Every object has two text forms. str() is for people and is what print() uses. repr() is for developers: for built-in types it aims to be unambiguous, often something you could paste back into Python. Containers use repr() for their contents, which is why print(["12"]) shows the quotes but print("12") doesn't.
When not to use it
Output meant for users should stay str(): nobody wants to see 'Ada' with quotes in a greeting. Use repr() when you are asking "what exactly is in this variable?", then take it out. For whole dicts and lists, pprint gives you repr with line breaks.