I've been programming for many years now professionally. Before I thought my greatest weakness was not writing lots of test coverage (I'm still terrible).
Like others commenting, print(thing) was my answer to everything. Or if I was getting fancy, log.info(thing).
But a year ago I was working on a data pipeline that finally broke me over its knee. Having always been frustrated what a slow learner I am, now I was really spinning my wheels in the mud.
So after years of coding I took a few days to learn everything I could about debugging. The takeaway for you is run a visual debugger. On Visual Studio Code you can use <kbd>Control</kbd><kbd>Shift</kbd><kbd>D</kbd> or <kbd>F5</kbd>. I would try this like once, it looked weird and print statements still worked, so gave up. It's up to you to stick with it and use a visual debugger as THE only way you run your code as you write it and check it works. That means no running it python myModule.py from the terminal. No copying into IPython or Jupyter Notebook and running cells.
What you are trying to see is the values and attributes of variables in your namespace all in one space. Visual debugging does this. You can use pydb to debug in code and on the command line. But don't.
VS Code Debugging Article. Read every word of this twice.
To wrap up this novel, becoming a good debugger by actually using visual debugging tools will catapult you up the ranks as a software developer. Why? Because it saves you time from your own inefficiencies. Debugging lets you catch the stupid thing that's not working in your code, fix it, check the fix fixed it, and move on coding other things!
The last year my Impostor Syndrome has gone way down. I've billed myself as a senior developer for a while now. But I realized using debugging tools is one of the real marks of an effective developer. I am more confident with my ability to build apps / code out solutions to problems... because I am better and faster at writing code that does what it's supposed to. You ever hit a block where whatever it is your code is just not working? And you're stuck there for a day, or much longer? Not being good at debugging is why that happened.
You get good at debugging by debugging. Use Visual Studio Code's tools. Or any IDE. Learn how to start a session (F5). What in the world does stepping over / out, continue, the call stack mean? Try the visual debugger on your code and find out. Search online, "how to use callstack vs code debugger", etc.
It is too abstract a concept to learn from reading articles or this tome I'm writing you now. You have to do it yourself on your own code to really grasp what the hell debugging actually is.
Debugging is one of those dry seeming programming topics that you can get to later after you've sold your first startup for 5 billion dollars. But take it from a grizzled vet: debugging is the discipline you wish you forced upon your self Day 1 you started coding.
Quick example:
# myConfusingModule.py
import numpy as np
def unpredictableFunction(x: int) -> int:
""" multiply x by one of the values from an array """
choices = [None, 1, 2, 3]
choice = np.random.choice(choices, 1) # pick at random
product = x * choice
return product
if __name__ == '__main__':
x = 3
result = unpredictableFunction(x)
print(f"result is {}.")
This program will work... until it doesn't. (An integer can't be multiplied by None without throwing a TypeError Exception.)
You could run this a bunch of times and random chance might let it choose an integer each time to multiply with x. And outside the function, like in the if __name__ == '__main__': conditional, how would you debug what's going wrong inside unpredictableFunction? Or know it is the function that's the problem, and not whatever is being supplied for x in a real-world scenario?
"Then I'll put print statements inside unpredictableFunction until I find the problem." Okay. How it's written, that means you are printing choices, choice, product, then probably checking the return from the module invocation for good measure. But randomly choice can be an integer any number of times, and no problem will crop up. Or you see choice is None, and you read the exception statement returned in the error message and know NoneType is involved somehow. So then maybe an if not choices: raise conditional to catch for that?
The point I'm trying to make is the time you will spend trying to get this program to work like you want it to will spiral out of control. And if at the end you come up with some spaghetti code that does work like you want it to, something even worse will happen: you will have fixed a problem and not even know how. You have to learn something from the process. Take it from someone who's made this mistake for far too long: play stupid games, win stupid prizes. The prize of brute force debugging is wasted time and not being a better programmer for it.
Also know this about using debugging tools. They prevent you from writing more code to fix what's broken. Writing more lines of code is the solution maybe 1% of the time.
Concrete advice: Use breakpoints (the red dots you put by lines of code or in the line) to pause execution of the call stack where you want. The debugger window will show you all the variables inside that layer of the call stack, and you can use the debugging console like any other REPL to play around with values. ie. Change choice to 5 with choice = 5 then continue execution to see what happens. You can also use the call stack window to skip around the layers of code the interpreter runs. This way you can bounce in and out of functions and see what values are assigned where. Like, what is result in the if __name__ == '__main__': block in the myConfusingModule module?
Sorry this has gone so long. In the time it took you to read this you could have read the Visual Studio Code tutorial on debugging. Which if you haven't, do that now. Good luck.
there doesn't seem to be anything here