├── dicegame ├── __init__.py ├── utils.py ├── die.py └── runner.py ├── solutions ├── dicegame │ ├── __init__.py │ ├── utils.py │ ├── die.py │ └── runner.py └── main.py ├── main.py ├── .gitignore ├── instructions.txt ├── LICENSE.txt └── README.md /dicegame/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /solutions/dicegame/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from dicegame.runner import GameRunner 2 | 3 | 4 | def main(): 5 | print("Add the values of the dice") 6 | print("It's really that easy") 7 | print("What are you doing with your life.") 8 | GameRunner.run() 9 | 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /solutions/main.py: -------------------------------------------------------------------------------- 1 | from dicegame.runner import GameRunner 2 | 3 | 4 | def main(): 5 | print("Add the values of the dice") 6 | print("It's really that easy") 7 | print("What are you doing with your life.") 8 | GameRunner.run() 9 | 10 | 11 | if __name__ == "__main__": 12 | main() 13 | -------------------------------------------------------------------------------- /dicegame/utils.py: -------------------------------------------------------------------------------- 1 | class UnnecessaryError(Exception): 2 | pass 3 | 4 | 5 | def i_just_throw_an_exception(): 6 | value = 1 7 | def some_inner_function(): 8 | value += 1 9 | 10 | some_value = "I don't know what you were expecting" 11 | raise UnnecessaryError("You actually called this function...") 12 | -------------------------------------------------------------------------------- /solutions/dicegame/utils.py: -------------------------------------------------------------------------------- 1 | class UnnecessaryError(Exception): 2 | pass 3 | 4 | 5 | 6 | 7 | def i_just_throw_an_exception(): 8 | value = 1 9 | def some_inner_function(): 10 | value += 1 11 | 12 | some_value = "I don't know what you were expecting" 13 | raise UnnecessaryError("You actually called this function...") 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Extra 28 | *.sw[po] 29 | -------------------------------------------------------------------------------- /instructions.txt: -------------------------------------------------------------------------------- 1 | Your boss has given you the following project to fix for a client. It's supposed to be a simple dice 2 | game where the objective of the game is to correctly add up the values of the dice for 6 consecutive turns. 3 | 4 | The issue is that a former programmer worked on it and didn't know how to debug effectively. It's now up to 5 | you to fix the errors and finally make the game playable. 6 | 7 | To play the game you must run the main.py file. 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Spiro Sideris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /solutions/dicegame/die.py: -------------------------------------------------------------------------------- 1 | # Just count the stupid dice 2 | import random 3 | 4 | def roll(dice): 5 | for die in dice: 6 | die.roll() 7 | 8 | class Die: 9 | """ 10 | This is always correct. Seriously, look away. 11 | """ 12 | 13 | def __init__(self): 14 | self.roll() 15 | 16 | def roll(self): 17 | self.value = int(random.random() * 6 + 1) 18 | 19 | def show(self): 20 | if self.value == 1: 21 | return("---------\n| |\n| * |\n| |\n---------") 22 | elif self.value == 2: 23 | return("---------\n|* |\n| |\n| *|\n---------") 24 | elif self.value == 3: 25 | return("---------\n|* |\n| * |\n| *|\n---------") 26 | elif self.value == 4: 27 | return("---------\n|* *|\n| |\n|* *|\n---------") 28 | elif self.value == 5: 29 | return("---------\n|* *|\n| * |\n|* *|\n---------") 30 | else: 31 | return("---------\n|* *|\n|* *|\n|* *|\n---------") 32 | 33 | @classmethod 34 | def create_dice(cls, n): 35 | return [cls() for _ in range(n)] 36 | -------------------------------------------------------------------------------- /dicegame/die.py: -------------------------------------------------------------------------------- 1 | # Just count the stupid dice 2 | import random 3 | 4 | def roll(dice): 5 | for die in dice: 6 | # XXX: I don't even know what this function does 7 | continue 8 | 9 | class Die: 10 | """ 11 | This is always correct. Seriously, look away. 12 | """ 13 | 14 | def __init__(self): 15 | self.roll() 16 | 17 | def roll(self): 18 | self.value = int(random.random() * 6 + 1) 19 | 20 | def show(self): 21 | if self.value == 1: 22 | return("---------\n| |\n| * |\n| |\n---------") 23 | elif self.value == 2: 24 | return("---------\n|* |\n| |\n| *|\n---------") 25 | elif self.value == 3: 26 | return("---------\n|* |\n| * |\n| *|\n---------") 27 | elif self.value == 4: 28 | return("---------\n|* *|\n| |\n|* *|\n---------") 29 | elif self.value == 5: 30 | return("---------\n|* *|\n| * |\n|* *|\n---------") 31 | else: 32 | return("---------\n|* *|\n|* *|\n|* *|\n---------") 33 | 34 | @classmethod 35 | def create_dice(cls, n): 36 | return [cls() for _ in range(n)] 37 | -------------------------------------------------------------------------------- /solutions/dicegame/runner.py: -------------------------------------------------------------------------------- 1 | from .die import Die 2 | from .utils import i_just_throw_an_exception 3 | 4 | class GameRunner: 5 | 6 | def __init__(self): 7 | self.dice = Die.create_dice(5) 8 | self.reset() 9 | 10 | def reset(self): 11 | self.round = 1 12 | self.wins = 0 13 | self.loses = 0 14 | 15 | @property 16 | def answer(self): 17 | total = 0 18 | for die in self.dice: 19 | total += die.value 20 | return total 21 | 22 | @classmethod 23 | def run(cls): 24 | count = 0 25 | runner = cls() 26 | while True: 27 | 28 | print("Round {}\n".format(runner.round)) 29 | 30 | for die in runner.dice: 31 | print(die.show()) 32 | 33 | guess = input("Sigh. What is your guess?: ") 34 | guess = int(guess) 35 | 36 | if guess == runner.answer: 37 | print("Congrats, you can add like a 5 year old...") 38 | runner.wins += 1 39 | count += 1 40 | runner.consecutive_wins += 1 41 | else: 42 | print("Sorry that's wrong") 43 | print("The answer is: {}".format(runner.answer)) 44 | print("Like seriously, how could you mess that up") 45 | runner.loses += 1 46 | count = 0 47 | print("Wins: {} Loses {}".format(runner.wins, runner.loses)) 48 | runner.round += 1 49 | 50 | if count == 6: 51 | print("You won... Congrats...") 52 | print("The fact it took you so long is pretty sad") 53 | break 54 | 55 | prompt = input("Would you like to play again?[Y/n]: ") 56 | 57 | if prompt.lower() == 'y' or prompt == '': 58 | continue 59 | else: 60 | break 61 | -------------------------------------------------------------------------------- /dicegame/runner.py: -------------------------------------------------------------------------------- 1 | from .die import Die 2 | from .utils import i_just_throw_an_exception 3 | 4 | class GameRunner: 5 | 6 | def __init__(self): 7 | self.dice = Die.create_dice(5) 8 | self.reset() 9 | 10 | def reset(self): 11 | self.round = 1 12 | self.wins = 0 13 | self.loses = 0 14 | 15 | def answer(self): 16 | total = 0 17 | for die in self.dice: 18 | total += 1 19 | return total 20 | 21 | @classmethod 22 | def run(cls): 23 | # Probably counts wins or something. 24 | # Great variable name, 10/10. 25 | c = 0 26 | while True: 27 | runner = cls() 28 | 29 | print("Round {}\n".format(runner.round)) 30 | 31 | for die in runner.dice: 32 | print(die.show()) 33 | 34 | guess = input("Sigh. What is your guess?: ") 35 | guess = int(guess) 36 | 37 | if guess == runner.answer(): 38 | print("Congrats, you can add like a 5 year old...") 39 | runner.wins += 1 40 | c += 1 41 | else: 42 | print("Sorry that's wrong") 43 | print("The answer is: {}".format(runner.answer())) 44 | print("Like seriously, how could you mess that up") 45 | runner.loses += 1 46 | c = 0 47 | print("Wins: {} Loses {}".format(runner.wins, runner.loses)) 48 | runner.round += 1 49 | 50 | if c == 6: 51 | print("You won... Congrats...") 52 | print("The fact it took you so long is pretty sad") 53 | break 54 | 55 | prompt = input("Would you like to play again?[Y/n]: ") 56 | 57 | if prompt == 'y' or prompt == '': 58 | continue 59 | else: 60 | i_just_throw_an_exception() 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pdb` Tutorial 2 | 3 | The purpose of this tutorial is to teach you the basics of `pdb`, the **P**ython **D**e**B**ugger for [Python2](https://docs.python.org/2/library/pdb.html) 4 | and [Python3](https://docs.python.org/3/library/pdb.html). 5 | It will also include some helpful tricks to make your debugging sessions a lot less stressful. 6 | 7 | --- 8 | 9 | #### Other translations 10 | 11 | The tutorial is written in english, but there are other translations available with help 12 | from the Python community: 13 | 14 | - [Korean](https://github.com/mingrammer/pdb-tutorial) 15 | - [Chinese](https://github.com/MartinLwx/pdb-tutorial) 16 | 17 | If you would like to see another other translation, or are interested in helping out with translating the tutorial, 18 | feel free to add to the [ongoing issues thread](https://github.com/spiside/pdb-tutorial/issues/9). 19 | 20 | --- 21 | 22 | 23 | The tutorial works best if you use Python 2.7 or Python 3.4 and I will highlight the 24 | differences between the two versions if a `pdb` 25 | command differs. To check what version of python you're using, type the following in your terminal: 26 | 27 | ```shell 28 | python --version 29 | ``` 30 | 31 | Now that you know your version, let's get to it! 32 | 33 | 34 | ## What is the purpose of a debugger? 35 | 36 | Before jumping into the code, we should have a brief discussion about the importance of debugging and using 37 | a debugging tool. For me, these three points highlight the importance of a debugger. 38 | 39 | With a debugger, you can: 40 | * Explore the state of a running program 41 | * Test implementation code before applying it 42 | * Follow the program's execution logic 43 | 44 | Using a debugger, you can set a [breakpoint](https://en.wikipedia.org/wiki/Breakpoint) at any point of 45 | your program to stop it and apply the three points above. Debuggers are very powerful tools and they 46 | can speed up the debugging process a lot faster than using simple `print()` statements everywhere. 47 | 48 | For those of you who are veteran programmers, you might agree with me that there is a 49 | correlation between the best programmers and the ones that know how to debug effectively. By debugging 50 | effectively, I mean being able to diagnose a problem and then treat the error with minimal difficulty. 51 | Using a debugger and learning how to use it properly will help you become an effective debugger. It will 52 | take some time before you feel comfortable navigating around in a debugging environment but the purpose 53 | of this tutorial is to get your feet wet before you start using `pdb` in your own code base! 54 | 55 | 56 | ## Playing the Game 57 | 58 | So we already talked about the purpose of a debugger and now it's time to see it in action. First, you 59 | should clone this repo if you haven't already done so. If you don't have `git` installed, I recommend using 60 | it (or some version of source control) and you can find out details on how to install `git` [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). 61 | Once you have `git` installed, clone the repo by entering the following in your terminal: 62 | 63 | ```shell 64 | git clone https://github.com/spiside/pdb-tutorial 65 | ``` 66 | 67 | **NB**: If that didn't work for you, you should follow Github's [cloning tutorial](https://help.github.com/articles/cloning-a-repository/). 68 | 69 | Now that you have the repo cloned, let's navigate to the root of the project and take a look at the instructions given: 70 | 71 | ```shell 72 | cd /path/to/pdb-tutorial 73 | ``` 74 | 75 | `file: instructions.txt` 76 | ``` 77 | Your boss has given you the following project to fix for a client. It's supposed to be a simple dice 78 | game where the objective of the game is to correctly add up the values of the dice for 6 consecutive turns. 79 | 80 | The issue is that a former programmer worked on it and didn't know how to debug effectively. 81 | It's now up to you to fix the errors and finally make the game playable. 82 | 83 | To play the game you must run the main.py file. 84 | ``` 85 | 86 | Seems easy enough! To begin, let's try playing the game to see what's wrong. To run the program, type the following in your 87 | terminal: 88 | 89 | ```shell 90 | python main.py 91 | ``` 92 | 93 | You should see something like this: 94 | 95 | ``` 96 | Add the values of the dice 97 | It's really that easy 98 | What are you doing with your life. 99 | Round 1 100 | 101 | --------- 102 | |* | 103 | | | 104 | | *| 105 | --------- 106 | --------- 107 | |* | 108 | | | 109 | | *| 110 | --------- 111 | --------- 112 | |* *| 113 | | | 114 | |* *| 115 | --------- 116 | --------- 117 | |* *| 118 | | | 119 | |* *| 120 | --------- 121 | --------- 122 | |* *| 123 | | * | 124 | |* *| 125 | --------- 126 | Sigh. What is your guess?: 127 | ``` 128 | 129 | Seems like the previous programmer had a sense of...humor? Nonetheless, let's enter 17 (since that is the total value of the dice). 130 | 131 | ``` 132 | Sigh. What is your guess?: 17 133 | Sorry that's wrong 134 | The answer is: 5 135 | Like seriously, how could you mess that up 136 | Wins: 0 Loses 1 137 | Would you like to play again?[Y/n]: 138 | ``` 139 | 140 | Weird. It said the answer is 5 but that's clearly wrong... Alright, maybe the dice addition is wrong but let's play the game again to 141 | figure it out. Looks like the prompt to play again is `'Y'` so let's enter that now. 142 | 143 | ``` 144 | Would you like to play again?[Y/n]: Y 145 | Traceback (most recent call last): 146 | File "main.py", line 12, in 147 | main() 148 | File "main.py", line 8, in main 149 | GameRunner.run() 150 | File "/Users/Development/pdb-tutorial/dicegame/runner.py", line 62, in run 151 | i_just_throw_an_exception() 152 | File "/Users/Development/pdb-tutorial/dicegame/utils.py", line 13, in i_just_throw_an_exception 153 | raise UnnecessaryError("You actually called this function...") 154 | dicegame.utils.UnnecessaryError: You actually called this function... 155 | ``` 156 | 157 | Ok weird, there was an exception that was thrown even though we used what was supposed to be a valid input. I think it's safe to 158 | say that the program is broken so let's start the debugging process! 159 | 160 | 161 | ## PDB 101: Intro to `pdb` 162 | 163 | It's time to finally work with python's very own debugger `pdb`. The debugger is included in python's standard library and we 164 | use it the same way we would with any python library. First, we have to import the `pdb` module and then call one of its methods 165 | to add a debugging breakpoint in the program. The conventional way to do this is to add the import **and** call the method at the same line you 166 | would like to stop at. This is the full statement you would want to include: 167 | 168 | ```python 169 | import pdb; pdb.set_trace() 170 | ``` 171 | 172 | Starting from version 3.7: The built-in function `breakpoint()` can be used instead of using `import pdb; pdb.set_trace()` 173 | 174 | ```python 175 | breakpoint() 176 | ``` 177 | 178 | The methods [`set_trace()`](https://docs.python.org/3/library/pdb.html#pdb.set_trace) and [`breakpoint()`](https://docs.python.org/3/library/functions.html#breakpoint) hard code a breakpoint 179 | where the method was called. Let's use the `set_trace()` method for this tutorial since it is supported in all python versions. Let's try it now by opening up the `main.py` file and adding the breakpoint 180 | on line 8: 181 | 182 | `file: main.py` 183 | ```python 184 | from dicegame.runner import GameRunner 185 | 186 | 187 | def main(): 188 | print("Add the values of the dice") 189 | print("It's really that easy") 190 | print("What are you doing with your life.") 191 | import pdb; pdb.set_trace() # add pdb here 192 | GameRunner.run() 193 | 194 | 195 | if __name__ == "__main__": 196 | main() 197 | ``` 198 | 199 | Cool, now let's try to run `main.py` again and see what happens. 200 | 201 | ```shell 202 | python main.py 203 | ``` 204 | ``` 205 | Add the values of the dice 206 | It's really that easy 207 | What are you doing with your life. 208 | > /Users/Development/pdb-tutorial/main.py(9)main() 209 | -> GameRunner.run() 210 | (Pdb) 211 | ``` 212 | 213 | There we go! We are now in the middle of the running program and we can start poking around. I think the first issue we should 214 | solve is the proper summation of the dice values. 215 | 216 | If you are familiar with Python's interpreter, a lot of that knowledge can be transferred to the `pdb` debugger. However, there will be 217 | a couple gotchas that we will get to in the advanced section. Regardless, let's learn a couple commands that will help us solve the 218 | addition issue. 219 | 220 | 221 | ## The 5 `pdb` commands that will leave you "speechless" 222 | 223 | Taken directly from the `pdb` documentation, these are the five commands that, once you learn them, you won't know how you lived 224 | without them. 225 | 226 | 1. `l(ist)` - Displays 11 lines around the current line or continue the previous listing. 227 | 2. `s(tep)` - Execute the current line, stop at the first possible occasion. 228 | 3. `n(ext)` - Continue execution until the next line in the current function is reached or it returns. 229 | 4. `b(reak)` - Set a breakpoint (depending on the argument provided). 230 | 5. `r(eturn)` - Continue execution until the current function returns. 231 | 232 | Notice that there are brackets around the last part of every keyword. The brackets indicate that the rest of the word is _optional_ when 233 | using the command prompt for `pdb`. This saves typing but a major gotcha is if you have a variable name such as `l` or `n`, then the 234 | `pdb` command takes precedence. That is, say you have a variable named `c` in your program and you want to know the value of `c`. Well, 235 | if you type `c` in `pdb`, you will actually be issuing the `c(ontinue)` keyword which executes the program and only stops if it encounters 236 | a break point! 237 | 238 | **NB**: I, and many other programmers, discourage the use of short variable names such as `a`, `b`, `gme`, etc. These carry no meaning 239 | and will confuse other people reading your code. I'm only demonstrating the issues you may encounter with `pdb` in the presence of 240 | shortened variable names. 241 | 242 | **NNB**: Another helpful tool is the following: 243 | `h(elp) - Without argument, print the list of available commands. With a command as an argument, print help about that command.` 244 | 245 | For the rest of the tutorial, I will be using the shortened version of the commands and if I use a command that I have not introduced 246 | here, I will explain what it does. So, let's begin with the first one. 247 | 248 | ### 1. l(ist) a.k.a. I'm too lazy to open the file containing the source code 249 | 250 | ``` 251 | l(ist) [first [,last]] 252 | List source code for the current file. Without arguments, list 11 lines around the current line 253 | or continue the previous listing. With one argument, list 11 lines starting at that line. 254 | With two arguments, list the given range; if the second argument is less than the first, it is a count. 255 | ``` 256 | 257 | **NB**: The above description was generated by calling `help` on `list`. To get the same output, in the `pdb` REPL type `help l`. 258 | 259 | Using `list`, we can examine the source code of the current file we are in. The arguments for `list` lets you specify a given range 260 | of lines you wish to see which can be helpful if you are in some weird 3rd party package and you are trying to figure out why they 261 | can't get string encoding working _true story_. 262 | 263 | **NB**: In Python 3.2 and above, you can type `ll` (long list) which shows you source code for the current function or frame. I use 264 | this all the time instead of `l` since it's much better knowing which function you are in than an arbitrary 11 lines around your 265 | current position. 266 | 267 | Let's try using `l` now. In your already open `pdb` prompt, type in `l` and look at the output: 268 | 269 | ``` 270 | (Pdb) l 271 | 4 def main(): 272 | 5 print("Add the values of the dice") 273 | 6 print("It's really that easy") 274 | 7 print("What are you doing with your life.") 275 | 8 import pdb; pdb.set_trace() 276 | 9 -> GameRunner.run() 277 | 10 278 | 11 279 | 12 if __name__ == "__main__": 280 | 13 main() 281 | [EOF] 282 | ``` 283 | 284 | If we want to see the whole file, we can call the list function with the range 1 to 13 like so: 285 | 286 | ``` 287 | (Pdb) l 1, 13 288 | 1 from dicegame.runner import GameRunner 289 | 2 290 | 3 291 | 4 def main(): 292 | 5 print("Add the values of the dice") 293 | 6 print("It's really that easy") 294 | 7 print("What are you doing with your life.") 295 | 8 import pdb; pdb.set_trace() 296 | 9 -> GameRunner.run() 297 | 10 298 | 11 299 | 12 if __name__ == "__main__": 300 | 13 main() 301 | ``` 302 | 303 | Unfortunately, we don't get that much information from this file alone but we do see that it is calling the `run()` method of the `GameRunner` 304 | class. At this point, you might be thinking, "Awesome, I'll just set a `pdb` in the run method in the `dicegame/runner.py` file !" That will 305 | work, but there's an even easier way using the `step` command we will discuss next. 306 | 307 | ### 2. `s(tep)` a.k.a let's see what this method does... 308 | 309 | ``` 310 | s(tep) 311 | Execute the current line, stop at the first possible occasion 312 | (either in a function that is called or in the current 313 | function). 314 | ``` 315 | 316 | Your current line of execution should still be on `:9` and you can tell the current line by looking at the `->` outputted by the `list` command. 317 | Let's call the `step` command and see what happens. 318 | 319 | ``` 320 | (Pdb) s 321 | --Call-- 322 | > /Users/Development/pdb-tutorial/dicegame/runner.py(21)run() 323 | -> @classmethod 324 | ``` 325 | 326 | Nice! We're currently in the `runner.py` file on line 21 which we can tell from this line: 327 | `> /Users/Development/pdb-tutorial/dicegame/runner.py(21)run()`. 328 | The problem is, we don't have much context so run the `list` command to checkout the method. 329 | 330 | ``` 331 | (Pdb) l 332 | 16 total = 0 333 | 17 for die in self.dice: 334 | 18 total += 1 335 | 19 return total 336 | 20 337 | 21 -> @classmethod 338 | 22 def run(cls): 339 | 23 # Probably counts wins or something. 340 | 24 # Great variable name, 10/10. 341 | 25 c = 0 342 | 26 while True: 343 | ``` 344 | 345 | Awesome! Now we have some more context on the `run()` method but we are currently on `:21`. Let's `step` in one more time so that we enter the method itself and 346 | then run the list command to see our current position. 347 | 348 | ``` 349 | (Pdb) s 350 | > /Users/Development/pdb-tutorial/dicegame/runner.py(25)run() 351 | -> c = 0 352 | (Pdb) l 353 | 20 354 | 21 @classmethod 355 | 22 def run(cls): 356 | 23 # Probably counts wins or something. 357 | 24 # Great variable name, 10/10. 358 | 25 -> c = 0 359 | 26 while True: 360 | 27 runner = cls() 361 | 28 362 | 29 print("Round {}\n".format(runner.round)) 363 | 30 364 | ``` 365 | 366 | As we can see, we are on a terribly named `c` variable that will cause us a major issue if we try to call it (remember the comment from earlier regarding the 367 | `c(ontinue)` command). We are just before the `while` loop so let's enter the loop and see what else we can uncover. 368 | 369 | ### 3. `n(ext)` a.k.a I hope this current line doesn't throw an exception 370 | 371 | ``` 372 | n(ext) 373 | Continue execution until the next line in the current function 374 | is reached or it returns. 375 | ``` 376 | 377 | From the current line, type the `n(ext)` command followed by `list` (notice a pattern) and let's observe what happens. 378 | 379 | **NB**: `n(ext)` allows you to skip over function calls while `s(tep)` allows you to step into a function call and pause at the first line of the called function. 380 | 381 | ``` 382 | (Pdb) n 383 | > /Users/Development/pdb-tutorial/dicegame/runner.py(27)run() 384 | -> while True: 385 | (Pdb) l 386 | 21 @classmethod 387 | 22 def run(cls): 388 | 23 # Probably counts wins or something. 389 | 24 # Great variable name, 10/10. 390 | 25 c = 0 391 | 26 -> while True: 392 | 27 runner = cls() 393 | 28 394 | 29 print("Round {}\n".format(runner.round)) 395 | 30 396 | 31 for die in runner.dice: 397 | ``` 398 | 399 | Now our current line on the `while True` statement! We can keep calling `next` indefinitely until the program throws an exception or terminates. Call `next` 3 more 400 | times to get to the `for` loop and then follow up `next` with `list`. 401 | 402 | ``` 403 | (Pdb) n 404 | > /Users/Development/pdb-tutorial/dicegame/runner.py(27)run() 405 | -> runner = cls() 406 | (Pdb) n 407 | > /Users/Development/pdb-tutorial/dicegame/runner.py(29)run() 408 | -> print("Round {}\n".format(runner.round)) 409 | (Pdb) n 410 | Round 1 411 | 412 | > /Users/Development/pdb-tutorial/dicegame/runner.py(31)run() 413 | -> for die in runner.dice: 414 | (Pdb) l 415 | 26 while True: 416 | 27 runner = cls() 417 | 28 418 | 29 print("Round {}\n".format(runner.round)) 419 | 30 420 | 31 -> for die in runner.dice: 421 | 32 print(die.show()) 422 | 33 423 | 34 guess = input("Sigh. What is your guess?: ") 424 | 35 guess = int(guess) 425 | ``` 426 | 427 | At this current point, if you continue to type the `next` command you will then iterate through the `for` loop for the length of the `runner.dice` 428 | attribute. We can take a look at the length of the `runner.dice` by calling the `len()` function around it in the `pdb` REPL which should return 5. 429 | 430 | ``` 431 | (Pdb) len(runner.dice) 432 | 5 433 | ``` 434 | 435 | Since the length is _only_ 5 items, we could iterate through the loop by calling `next` 5 times, but let's say there were 50 items to iterate over, or even 10,000! 436 | A better option would be to set a break point and then `continue` to that break point instead. 437 | 438 | 439 | ### 4. `b(reak)` a.k.a I don't want to type `n` anymore 440 | 441 | ``` 442 | b(reak) [ ([filename:]lineno | function) [, condition] ] 443 | Without argument, list all breaks. 444 | 445 | With a line number argument, set a break at this line in the 446 | current file. With a function name, set a break at the first 447 | executable line of that function. If a second argument is 448 | present, it is a string specifying an expression which must 449 | evaluate to true before the breakpoint is honored. 450 | 451 | The line number may be prefixed with a filename and a colon, 452 | to specify a breakpoint in another file (probably one that 453 | hasn't been loaded yet). The file is searched for on 454 | sys.path; the .py suffix may be omitted. 455 | ``` 456 | 457 | We're only going to pay attention to the first two paragraphs of `b(reak)`'s description in this tutorial. Like I mentioned in the previous section, we want 458 | to set a break point past the `for` loop so we can continue to navigate through the `run()` method. Let's stop on `:34` since this has the input function 459 | which will break and wait for a user input anyways. To do this, we can type `b 34` and then `continue` to the break point. 460 | 461 | ``` 462 | (Pdb) b 34 463 | Breakpoint 1 at /Users/Development/pdb-tutorial/dicegame/runner.py(34)run() 464 | (Pdb) c 465 | 466 | [...] # prints some dice 467 | 468 | > /Users/Development/pdb-tutorial/dicegame/runner.py(34)run() 469 | -> guess = input("Sigh. What is your guess?: ") 470 | ``` 471 | 472 | We can also take a look at the break points that we have set by calling `break` without any arguments. 473 | 474 | ``` 475 | (Pdb) b 476 | Num Type Disp Enb Where 477 | 1 breakpoint keep yes at /Users/Development/pdb-tutorial/dicegame/runner.py:34 478 | breakpoint already hit 1 time 479 | ``` 480 | 481 | To clear your break points, you can use the `cl(ear)` command followed by the breakpoint number which is found in the leftmost column of the above 482 | output. Let's clear the breakpoint now by calling the `clear` command followed by 1. 483 | 484 | **NB**: You can also clear all the breakpoints if you don't provide any arguments to the `clear` command. 485 | 486 | ``` 487 | (Pdb) cl 1 488 | Deleted breakpoint 1 at /Users/Development/pdb-tutorial/dicegame/runner.py:34 489 | ``` 490 | 491 | From here we can call `next` and execute the `input()` function. Let's just type 10 for our guess and once we are back in the `pdb` REPL, call `list` so we can see 492 | the next few lines. 493 | 494 | ``` 495 | (Pdb) n 496 | Sigh. What is your guess?: 10 497 | > /Users/Development/pdb-tutorial/dicegame/runner.py(35)run() 498 | -> guess = int(guess) 499 | (Pdb) l 500 | 30 501 | 31 for die in runner.dice: 502 | 32 print(die.show()) 503 | 33 504 | 34 guess = input("Sigh. What is your guess?: ") 505 | 35 -> guess = int(guess) 506 | 36 507 | 37 if guess == runner.answer(): 508 | 38 print("Congrats, you can add like a 5 year old...") 509 | 39 runner.wins += 1 510 | 40 c += 1 511 | 512 | 513 | ``` 514 | 515 | Remember that we are trying to find out why our guess wasn't correct on our first playthrough. It seemed like there was an error with the `guess == runner.answer` 516 | equality condition. We should check to see what the `runner.answer()` method is doing in case there might be an error there. Call `next` and then let's call `step` 517 | to _step_ into the `runner.answer()` method. 518 | 519 | ``` 520 | (Pdb) s 521 | --Call-- 522 | > /Users/spiro/Development/mobify/engineering-meeting/pdb-tutorial/dicegame/runner.py(15)answer() 523 | -> def answer(self): 524 | (Pdb) l 525 | 10 def reset(self): 526 | 11 self.round = 1 527 | 12 self.wins = 0 528 | 13 self.loses = 0 529 | 14 530 | 15 -> def answer(self): 531 | 16 total = 0 532 | 17 for die in self.dice: 533 | 18 total += 1 534 | 19 return total 535 | 20 536 | ``` 537 | 538 | I think I found the issue! On line 18, it doesn't look like the `total` variable is adding up the values of the dice like we want it to. Let's see if we can fix that by 539 | checking whether a `die` has an attribute which would contain its value. To get to line 18, you can either set a break point or just call `next` until you 540 | hit the first iteration. Once you're on `:18`, let's call the `dir()` function on the `die` instance and check what methods and attributes it has. 541 | 542 | ``` 543 | -> total += 1 544 | (Pdb) dir(die) 545 | ['__class__', '__delattr__', [...], 'create_dice', 'roll', 'show', 'value'] 546 | ``` 547 | 548 | There is a `value` attribute after all! Let's call that and see what returns (remember, this value will probably be different than mine). And just for fun, 549 | let's make sure it is equal to the value that the die is showing by calling the `show()` method as well. 550 | 551 | ``` 552 | (Pdb) die.value 553 | 2 554 | (Pdb) die.show() 555 | '---------\n|* |\n| |\n| *|\n---------' 556 | ``` 557 | 558 | **NB**: If you want the newline character `\n` to print as a newline, call `print()` with `die.show()` as its argument. 559 | 560 | It looks like it works as expected and we're ready to fix the answer method. However, some of us may want to continue with the debugging process and catch all the 561 | errors in one go. Unfortunately, we are once again stuck in this for loop. You might think to set a break point at `:19` and then call `continue` but there is actually 562 | a better way in this case. 563 | 564 | ### 5. `r(eturn)` a.k.a. I want to get out of this function 565 | 566 | ``` 567 | r(eturn) 568 | Continue execution until the current function returns. 569 | ``` 570 | 571 | The `return` is a great _power user_ command that let's you examine the final outcome of a function. While you could set a breakpoint at the return call, the 572 | `return` pdb command will help if there are multiple return statements in a single function since it only follows the path of execution for a single return. Let's 573 | call the `return` command and get to the end of the function. 574 | 575 | ``` 576 | (Pdb) r 577 | --Return-- 578 | > /Users/Development/pdb-tutorial/dicegame/runner.py(19)answer()->5 579 | -> return total 580 | (Pdb) l 581 | 14 582 | 15 def answer(self): 583 | 16 total = 0 584 | 17 for die in self.dice: 585 | 18 total += 1 586 | 19 -> return total 587 | 20 588 | 21 @classmethod 589 | 22 def run(cls): 590 | 23 # Probably counts wins or something. 591 | 24 # Great variable name, 10/10. 592 | (Pdb) 593 | ``` 594 | 595 | To check the value of the returned `total` variable, you can call `total` here or look at the final value in line below the `--Return--` output. Now, to return back 596 | to the `run()` method, call the `next` command and you'll be back in your happy place. 597 | 598 | At this point, you can exit the `pdb` debugger by calling `exit()` **OR** `CTRL+D` (same as the Python REPL). With these five commands, you should be able to figure 599 | out a couple other bugs and then follow along with a bit more advanced `pdb` examples. 600 | 601 | 602 | ## Advanced `pdb` topics 603 | 604 | Here are a couple advanced `pdb` commands that you can also use. 605 | 606 | ### The `!` (bang) command 607 | 608 | ``` 609 | ! 610 | Execute the (one-line) statement in the context of the current stack frame. 611 | ``` 612 | 613 | The bang command (`!`) lets `pdb` know that the following statement will be a Python command and not a `pdb` command. Where this is helpful is in the `run()` method 614 | with the `c` variable. Like I mentioned in the beginning of the tutorial, calling `c` in `pdb` will issue the `continue` command. Navigating in your `pdb` REPL, stop 615 | at `:26` in the `runner.py` file and from that point you can prefix `c` with the `!` command and see what happens. 616 | 617 | ``` 618 | (Pdb) !c 619 | 0 620 | ``` 621 | 622 | We get the intended result, since `:25` assigned `c = 0`! 623 | 624 | ### The `commands` command 625 | 626 | ``` 627 | commands [bpnumber] 628 | (com) ... 629 | (com) end 630 | (Pdb) 631 | 632 | Specify a list of commands for breakpoint number bpnumber. 633 | ``` 634 | 635 | `commands` will run python code or pdb commands that you specified whenever the stated breakpoint number is hit. Once you start the `commands` block, the prompt changes to `(com)`. The code/commands you write here function as if you had typed them at the `(Pdb)` prompt after getting to that breakpoint. Writing `end` will terminate the command and the prompt changes back to `(Pdb)` from `(com)`. I have found this of great use when I need to monitor certain variables inside of a loop as I don't need to print the values of the variables repeatedly. Let's see an example. Make sure to be at the root of the project in your terminal and type the following: 636 | 637 | ``` 638 | python -m pdb main.py 639 | ``` 640 | 641 | Reach line `:8` and `s(tep)` into the `run()` method of the GameRunner class. Then, set up a breakpoint at `:17`. 642 | ``` 643 | > /Users/Development/pdb-tutorial/main.py(8)main() 644 | -> GameRunner.run() #This is line 8 in main.py 645 | (Pdb) s 646 | --Call-- 647 | > /Users/Development/pdb-tutorial/dicegame/runner.py(21)run() 648 | -> @classmethod 649 | (Pdb) b 17 650 | Breakpoint 4 at /Users/Development/pdb-tutorial/dicegame/runner.py:17 651 | ``` 652 | This sets up the breakpoint, which has been given the number `4`, at the start of the loop inside the `answer()` method which is used to calculate the total values of the dice. Now, let's us use `commands` to print the value of the variable `total` when we hit this breakpoint. 653 | 654 | ``` 655 | (Pdb) commands 4 656 | (com) print(f"The total value as of now is {total}") 657 | (com) end 658 | ``` 659 | We have now set up `commands` for breakpoint number 4 which will execute when we reach this breakpoint. Let us `c(ontinue)` and reach this breakpoint. 660 | 661 | ``` 662 | (Pdb) c 663 | [...] # You will have to guess a number 664 | The total value as of now is 0 665 | > /Users/Development/pdb-tutorial/dicegame/runner.py(17)answer() 666 | -> for die in self.dice: 667 | (Pdb) 668 | ``` 669 | 670 | We see that out print statement executed upon reaching this breakpoint. Let's `c(ontinue)` again and see what happens. 671 | 672 | ``` 673 | (Pdb) c 674 | [...] 675 | The total value as of now is 1 676 | > /Users/Development/pdb-tutorial/dicegame/runner.py(17)answer() 677 | -> for die in self.dice: 678 | (Pdb) 679 | ``` 680 | The `commands` command executes upon reaching the breakpoint again. You can see how this might be useful especially during loops. 681 | 682 | ### `pdb` Post Mortem 683 | 684 | ``` 685 | pdb.post_mortem(traceback=None) 686 | Enter post-mortem debugging of the given traceback object. If no traceback is given, it uses the one of the exception that is currently being handled 687 | (an exception must be being handled if the default is to be used). 688 | 689 | pdb.pm() 690 | Enter post-mortem debugging of the traceback found in sys.last_traceback. 691 | ``` 692 | 693 | While both methods may look the same, `post_mortem() and pm()` differ by the traceback they are given. I commonly use `post_mortem()` in the `except` block. 694 | However, we will cover the `pm()` method since I find it to be a bit more powerful. Let's try and see how this works in practice. 695 | 696 | Open up the python REPL by typing `python` in your shell in the root of this project. From there, let's import the `main` method from the `main` module and import `pdb` 697 | as well. Play the game until the we get the exception after trying to type `Y` to continue the game. 698 | 699 | ``` 700 | >>> import pdb 701 | >>> from main import main 702 | >>> main() 703 | [...] 704 | Would you like to play again?[Y/n]: Y 705 | Traceback (most recent call last): 706 | File "main.py", line 12, in 707 | main() 708 | File "main.py", line 8, in main 709 | GameRunner.run() 710 | File "/Users/Development/pdb-tutorial/dicegame/runner.py", line 62, in run 711 | i_just_throw_an_exception() 712 | File "/Users/Development/pdb-tutorial/dicegame/utils.py", line 13, in i_just_throw_an_exception 713 | raise UnnecessaryError("You actually called this function...") 714 | dicegame.utils.UnnecessaryError: You actually called this function... 715 | ``` 716 | 717 | Now, let's call the `pm()` method from the `pdb` module and see what happens. 718 | 719 | ``` 720 | >>> pdb.pm() 721 | > /Users/Development/pdb-tutorial/dicegame/utils.py(13)i_just_throw_an_exception() 722 | -> raise UnnecessaryError("You actually called this function...") 723 | (Pdb) 724 | ``` 725 | 726 | Look at that! We recover from the point where the last exception was thrown and are placed in the `pdb` prompt. From here, we can examine the state the program was in 727 | before it crashed which will help you in your investigation. 728 | 729 | **NB**: You can also start the `main.py` script using `python -m pdb main.py` and `continue` until an exception is thrown. Python will automatically enter `post_mortem` 730 | mode at the uncaught exception. 731 | 732 | 733 | ## The End 734 | 735 | Congrats on making it to the end and thank you for following along in this tutorial! If you have any comments, critiques, or additional advanced examples, I'm open to pull requests. 736 | 737 | --------------------------------------------------------------------------------