## Intro I’ve been making software for a while at this point, and I’ve lately begun to realize a common theme in the questions I get from more junior developers: they don’t want to know so much about the code I’m working on, as the tools I use to work on it. So what follows will be essays talking about the tools and skills I use to work on code. My preferred tools are Python as a coding language, git as a version control system, and vim as an editor, but I intend for this to be broadly applicable; I don’t want you to use exactly the tools I’m using, but rather to learn to apply the underlying concepts to the tools you’re using. > **Note:** this was written before the advent of generative AI systems. I think that they inhibit learning, are inherently conservative if not outright *archaic*, and are, at this point, an ecological disaster. But they’ve colonized the brains of many execs and, often, many junior engineers. At this point, I *hope* to get these questions, because they indicate a junior who wants to learn. ## Getting great with your editor Whatever editor it is, your editor is your primary and foundational tool for making code. It’s your 8-inch chef’s knife, your Pigma Micron 005, your handsaw, whatever. You need to learn what you can do with your editor, and learn how to fold that into your workflow. At minimum, you want to be able to rapidly **open files**, **edit files**, **save files**. You want to learn to do this via keyboard commands, not via mousing around. Beyond that, you want to learn what tools your editor provides for **autocompletion**, **code sense**, and **running tests**. These are things that will speed up the parts of your coding that you have to do a million times a day, and free up more of your brain for the hard part of coding, which is thinking about the problem at hand and how to express a solution for it in your language of choice. A [wise friend](https://grimoire.ca) once told me off when I said that I didn’t like IDEs (integrated development environments, tools like PyCharm or IntelliJ IDEA) and rightly pointed out that I like IDEs, as long as I’ve cobbled them together from parts myself. So with that caveat, let me talk about my tools and how I handle the basic activities above. I use vim, version 8.2 as of this writing, inside tmux, version 3.2a as of this writing. Typically I have my screen split such that there’s about ⅔rds vim on the left, and ⅓rd terminal on the right. I’ll use the terminal on the right for any poking around I have to do, but most especially for running my tests and linting tools. I have a number of vim plugins configured, including one (my own fork of vim-tmuxify) that lets me, from vim, send a custom-defined command to another tmux pane. So I will open vim in the wide pane, and edit my code there, and have set that custom command to whatever runs the tests in the project (sometimes that’s `make test`, sometimes it’s `pytest`, sometimes it’s `tox`, sometimes it’s `./bin/test`, whatever). I bind this to an easy key sequence (comma-enter, in my case) and run it just as frequently as I need to. (One of the things I like about this setup is that it keeps the test-running in the realm of “plain old terminal commands”, which allows me to write Readmes that *should* be usable by anyone, regardless of their IDE and editor set-up. It keeps the test-running in the lingua-franca of Unix-style tooling.) You don’t need to do this! You need to find how your editor best handles running tests. If you have to tab over to a terminal, OK, sobeit, but if you can run your tests inside your editor, that’s much better. There’s research showing that just cmd-tabbing between apps spikes the likelihood of losing focus and forgetting what you were doing. If you can stay in one visual context and just look down or to the right or wherever to where the tests are, it will help you stay focused. For autocompletion and code sense, I used to use another plugin, YouCompleteMe. Lately, it’s broken, so right now, I’m working on getting these capacities back into my flow! Autocomplete and code sense are low-value (but not zero-value) to me usually, so this hasn’t been a priority. As to opening and moving around between files, I use two other vim plugins (ctrlp.vim and vim-buffergator) that let me rapidly open files by fuzzy-matching on filenames, and clearly and easily see which files I have open and which one I’m in, including rapid switching between the last two files I’ve opened. This speaks to a common (*very* common) need when coding: often I have to coordinate my thinking between two different modules that interact with each other, and so I have to go back and forth between file A and file B. These can be code and its tests, or perhaps a `views.py` and a `models.py` in a Django project. Either way, having the ability to smoothly and rapidly switch, with zero brain-cost incurred, between two files, is very valuable. Again: find out how to do this in your editor. Sometimes, it’s worth spending a day just working on your tools, editing your configs and trying out some new plugins, or whatever your equivalent is. I frequently add new plugins to my vim config, and try them out. If I can’t fold them into my muscle memory, I get rid of them again, but it’s always worth putting a little time in to see if there’s not a better way to do things. I guess the moral here is this: the more steps it takes to do something, the more chances you have to lose track of your intent, and get derailed. Those micro-derailments add up. Make your breaks (and you *do* need them!) intentional, not a byproduct of your tools. ## Reading stack traces This is a short but vital point. When you’re developing software, sometimes your code will blow up with an unhandled error of some sort. The resultant pages of junk in your terminal can be overwhelming and frustrating, but it’s actually full of crucial information, if you know how to read it. (Every language outputs stack traces a bit differently. I’ll focus on Python, but the underlying ideas are portable.) You can think of your program as one “entrypoint” function that calls some series of other functions. Each function called gets put on a stack, and when it’s done, it’s popped off the stack again. Over time, the stack will go from the entrypoint, to having more and more things on it, with the top bubbling up and down as various functions get added, resolve, and then new ones are called on the next lines of your code. When an unhandled error happens, the whole stack comes crashing down, but the computer gives you a snapshot of what it was like at that moment, so you can get some idea of what went wrong. So the stack trace is telling you, at the moment the program crashed, what functions were unfinished, in what order, and what particular lines of code each were at. In Python, this looks something like this (example from Wikipedia): ``` Traceback (most recent call last):   File "tb.py", line 15, in <module> a()   File "tb.py", line 3, in a j = b(i)   File "tb.py", line 9, in b c()   File "tb.py", line 13, in c error() NameError: name 'error' is not defined ``` The first line just tells you this is a stack trace. The last line tells you the particular error that happened. Every pair of lines in between is a frame (that is, a function) on the stack at the moment it crashed. The bottom one is the most-recent function, the one that was immediately running. Tracing up, frame by frame, you get the one that called the frame below. So in the simplest case, you look at the last frame, check the filename and the line number, go there in your editor, and see if there’s something obvious you can fix. If there isn’t, you move up a frame, and see if there’s something about how you called that other function that was wrong. Remember that you might have incorrect or malformed data, as well (more about that when I write “using debuggers”). However, in a real project, there’s usually a lot more to a stack trace, and it can be overwhelming. But your job is actually simpler than it may seem! When I have a real project with a huge long stack trace, I *don’t have to look at every frame*. I only look at frames *for code I’ve written*. Over 90% of the time, I’m not going to be finding a bug in some library code, but rather a bug in what I wrote in the last 20 minutes. So, I start at the most recent frame, just to see what the error is, and then step up through the frames *only pausing at the ones in files I wrote*. The most-recent frame may come from some library, or from my code, but either way I start with the error, and then move back through the frames I can directly edit. ## Using debuggers Something I’ve heard from junior devs more than a couple times is that they have never really learned how to use an interactive debugger. Obviously, each language and each debugger is different, but I’ll try to give some general principles and ways of thinking about the tools. I’ll be talking here about “good old fashioned” imperative languages, like C, Python, or JavaScript, as those are at the intersection of “have debuggers I have used” and “you are likely to use them for work”. Besides, you shouldn’t ever need a debugger for your Lisp or Haskell, right? Right? (That’s a “functional programming is perfect” joke.) OK, so here’s how I think about a running program in these sorts of languages: at each line in the code, there is a command about to happen (the line of code, visible in front of you) and a whole bunch of variables with particular values (that is, state, implicit and invisible). The point of the debugger is to help you see the parts of this you can’t see, i.e. the state, line by line. Your basic tools with a debugger are these: - *Set a breakpoint.* That is, let your code run as normal until it gets to the line you’re particularly interested in. Typically in Python, this is done by altering your code to import `pdb`, and then running `pdb.set_trace()` at the relevant point in your code. - *Print a variable value.* This is how you can interrogate the state at the moment of the line you’re currently on. This is done once you are in an interactive debugging session. - *Next.* This runs the next line of code, as though it were an atomic unit. This is done once you are in an interactive debugging session. - *Step in.* This steps into any function call on the next line of code, allowing you to *not* treat it as an atomic unit, and see what happens inside it. This is done once you are in an interactive debugging session. - *Continue.* This gets the code running as normal again, until it comes across another breakpoint, or exits. This is done once you are in an interactive debugging session. With these basics in hand, you can start your debugger journey. Let’s look at a toy example. Say I’ve got some code that is supposed to return a dict with a new key added, but it’s behaving strangely: I keep getting more and more keys added to the dict over the lifetime of the program. (If you spot the error, please, still, read along to see how we get there with a debugger; this is, after all, a toy example!) ```python def frobulate_dict(     k: str,     v: Any,     d: Dict[str, Any] = {} ) -> Dict[str, Any]:     d[k] = v     return d ``` So step zero: through knowing about my code, I’ve isolated the problem to this function, at least *probably*. Now, step one: insert a debugger breakpoint. ```python def frobulate_dict(     k: str,     v: Any,     d: Dict[str, Any] = {} ) -> Dict[str, Any]:     import pdb     pdb.set_trace()     d[k] = v     return d ``` Now we run our code, and suddenly it drops into the debugger REPL at that line: ``` ➜ python example.py > /Users/kit/Desktop/example.py(11)frobulate_dict() -> d[k] = v (Pdb) ``` And here we have a flashing cursor and the chance to examine the contents of the heretofore invisible state. Let’s just look at the value of `d`: ``` (Pdb) !d {} ``` That’s a `!` to make sure that the next characters are interpreted as code, and not as a debugger command, followed by `d` which is just the variable pointing to our dict. OK, so on the first run, as expected, the dict is empty before we do anything to it. Let’s continue, and get to the second run, since this error cropped over time. ``` (Pdb) c > /Users/kit/Desktop/example.py(11)frobulate_dict() -> d[k] = v ``` And we’re back at the breakpoint. Let’s look at d again: ``` (Pdb) !d {'foo': 'bar'} ``` Oops! Looks like we didn’t get the default on this second run, but rather the already-mutated dict from last time. This points us, through some research and reasoning, to the culprit: we instantiated the dict in the default arguments, which is a moderately-well-known Python gotcha that will lead to this sort of error (because the instance persists between calls). Now we can just hit `q` (“quit”) and force the code to error out, and get back to fixing it. This is a toy example, but it should get you started, at least. Start playing around with this, and then docs will make more sense to you as you start to get used to using a debugger. I’ll note: JavaScript usually runs debuggers in the context of a browser, not a terminal, and it usually has to handle many more ways for the code to start (“entrypoints”). This makes debugging in JS sometimes more difficult, but it is similar enough that you should be able to get started. ## Leaky abstractions (a.k.a. “How to know only what you need to know”) *To do.* ## A mental model of Python imports *To do.* ## A mental model of git *To do.*