When used well, TODOs can help you write more robust code and minimise unhandled edge cases. When used badly, they disappear into your codebase and are forgotten, marking problems which will never be fixed.
Here’s what I think of as a best-practice system for using TODOs. It treats them like sticky notes and is made up of two rules and four actions. I call it “TODO Zero”.
The two rules
There are only two rules, and everything flows from them.
Capture every useful thought in a TODO.
Leave no TODOs in shipped code.
The four actions
Something must be done with a TODO which was created because of Rule 1 before it is deleted because of Rule 2, so the rules give rise to the four actions.
Create an issue (or ticket) from it.
Convert it to a note.
These rules and actions combine to create a simple system which works like this.
When you’re writing code and you have a useful idea or thought which isn’t directly relevant to the code you’re writing at that instant, record it as a TODO. Then as soon as you’ve done that forget about it and go back to the code you were writing.
Treat a TODO as a temporary reminder, like a sticky note. It serves two purposes. The first is to make sure that ideas and insights which you can’t act on straight away aren’t forgotten. The second is to clear your mind of thoughts that aren’t relevant to what you’re doing right at that instant.
Over your work session, review the TODOs you’ve written. Your aim for each one is to use one of the four actions to deal with it, and then delete it. Just like sticky notes, TODOs are thrown out as soon as they’ve served their purpose as a reminder, and just like sticky notes, they don’t make it into the final product.
Before your code reaches the branch from which you ship (i.e. release or deploy), all the TODOs you’ve created should be deleted. No TODOs should survive in your finished code, which is where the name TODO Zero comes from.
Let’s dive in
That was the high-level summary, now stay with me as I explain how it works, why it works and how to best make it work for you.
First, let’s start with a look at the most common problem TODO Zero saves you from – The Forgotten TODO.
The Forgotten TODO
A TODO usually marks something that the person who left it didn’t have the time, energy or knowledge to take care of.
Let’s say you’re coding productively and you don’t want to slow down to handle an unlikely edge case, so you quickly throw in a comment like this and move on:
# TODO: Handle an empty list.
Or you’re racing the clock on a deadline. The function you’ve just finished is unpleasantly complicated but it works, and so you add a note like this above it:
# TODO: Clean this function up.
Or maybe an aspect of what you’re doing is a bit beyond your current capabilities, so you leave a comment like this behind hoping that someone with the right skills will see it one day and sort it out:
# TODO: Optimise for large querysets.
It’s easy to write a TODO and tell ourselves someone will get to it sometime. But is that what really happens once we finish working with that code? Does someone get to it, ever? In my experience, the answer is usually “no”.
How often have you stumbled across a TODO in some rarely visited code, only to ignore it and move on because it’s not relevant to the task at hand? How many people before you have ignored that same comment, just as you did? And so there it stays through the ages, forgotten.
TODO Zero stops TODOs being forgotten because it requires us to do something with them rather than letting them sneak into shipped code.
Let’s see how to put it into practice!
Rule 1: Capture every useful thought in a TODO
If you’re anything like me, you’ll have thoughts and realisations about the code you’re working on faster than you can implement them.
Many of these will be useful, and easily forgotten. If you try to remember them, you’ll find that they get in the way of you focusing properly on the code you’re writing, and you’ll still forget some. The less you try to remember, the lower your cognitive load and the more clearly you can think.
It’s much better to capture them in TODOs as they come to you, as if you were jotting them down quickly on sticky notes. Do this and you can come back and take care of each one when you have time and can give it your full attention.
Let’s say you’ve just finishing writing the signature of the
function when two edge cases it will need to handle come to mind. You can
capture them like this:
def display_errors(error_list): # TODO: Handle a really long list. Truncate and add count of hidden items? # TODO: Handle error messages with >100 characters.
It’s better to get the main path through the function fleshed out before shifting focus to handle those edge cases, so you make them into TODOs and forget about them for now.
Then let’s say you’re only two lines into
display_errors when you have a
realisation about the thread-safety of the
build_pdf function you wrote an
hour ago. Again, capture it in a TODO. It doesn’t matter where. You can put it
right where you’re working for the time being and move it later:
def display_errors(error_list): for error in error_list: print(error) # TODO: Ensure that build_pdf function is thread-safe. # TODO: Handle a really long list. Truncate and add hidden item count? # TODO: Handle error messages with >100 characters.
When you use TODOs in this way, you ensure that every realisation and edge case which occurs to you is recorded and will be dealt with. Once recorded they can’t fall through the cracks, so your code will be more robust.
Because a TODO is now just a note to yourself, don’t take time making it detailed or perfect. You’re the only one who will read it. As long as you can understand it, that’s all that matters. Just like a sticky note.
Anyone familiar with David Allen's Getting Things Done (GTD) methodology will recognise what I’ve just described as what he calls “ubiquitous capture”. Which is really just another way of saying that if you have a good idea, record it somewhere so you don’t forget it.
Rule 2: Leave no TODOs in shipped code
The TODOs you create represent tasks you need to do, problems you need to solve or ideas you need to consider as part of the work you’re doing.
Every single one should be addressed and then removed before the code reaches the branch you ship from.
By addressing them all, you gain the confidence of knowing that every potential problem or issue which occurred to you has been dealt with, so your code is as thorough as you can make it.
Addressing a TODO requires you to do something with it so that it is no longer needed. That means applying one of the four actions – disregard it, do it, create an issue from it or convert it to a note. I’ll explain each of them in more detail shortly.
As you progress through the work you’re doing, take some time now and then to look over the TODOs you’ve made. When the time is right for each one, do something with it and then delete it.
I use this Makefile target which lists all TODOs to help with that:
todos: @grep -r -v "| grep TODO" . | grep TODO
You should find that as you get closer to finishing there are fewer and fewer remaining. When you finish, ensure that there are none left at all. Your code is not complete until they have all been addressed and removed.
Again, think of a TODO like a sticky note. It’s a scrappy short-term reminder that doesn’t belong in a finished product and is disposed of when it has served its purpose.
Now let’s look at each of the four actions.
Action 1: Disregard it
Sometimes you’re wrong.
When you record all the thoughts and ideas that cross your mind, once you come back later to give them your full attention, it’s inevitable that some will turn out to be junk, or mistakes, or just not worth doing.
Maybe you thought you saw an edge case where there wasn’t one. Maybe what seemed like a good idea turns out to be a bad idea. Maybe you saw an opportunity for an optimisation which would actually be too time-consuming to be worth doing.
Not to worry. When you come across a TODO like this, disregard it, delete it, and move on.
Action 2: Do it
Sometimes you’re right.
Often a TODO contains a good idea that’s worth doing, and worth doing now.
Maybe it’s an easy optimisation. Maybe it’s dealing with an edge case.
If it’s necessary to complete your work, or if it’s not but you think it’s worth doing now anyway, do it. Then once you’ve done it, delete that TODO.
Action 3: Create an issue (or ticket) from it.
Sometimes it can wait for later.
There will be some TODOs which describe something which will need to be done eventually, but which can wait until after your code is shipped.
For each of these, create an issue from it in the project’s issue tracker, then delete the TODO.
Although some people argue that in cases like this leaving TODOs in shipped code is an effective way to track tasks that need to be done, I disagree. I’m a believer in the benefits of having a single source of truth for work to be done. When an issue tracker isn’t tracking all the work for a project, you undermine its effectiveness, and I say that because my experience is that what’s not in the issue tracker doesn’t get done.
Action 4: Convert it to a note.
Sometimes it’s just not going to happen.
Some TODOs fall into the category of “in a perfect world we’d address that, but it’s never likely to be important enough to worry about”.
In my experience, these are often the ones which flag edge cases which are either very unlikely to arise, or won’t have worrying consequences when they do, or both.
It’s not appropriate to create issues about these in your issue tracker, because you don’t actually intend to do anything about them.
But it is helpful to convert them into NOTEs which point out the shortcomings of the code for the benefit of people who will work with it in the future.
As an example, you may have made this TODO as you were writing a function which is never expected to be called with a negative number:
# TODO: Should this function be able to handle negative numbers?
If you’ve decided that it wouldn’t be worth the effort to add handling of negative numbers, you would replace the TODO with a NOTE like this:
# NOTE: This function does not work for negative numbers.
This conveys the information about the function’s limitation, but unlike a TODO it doesn’t imply that it will ever be attended to.
Making an issue and a note
Sometimes you might decide to create an issue (Action 3), and also convert the TODO into a NOTE (Action 4) as a marker for the issue.
This is a useful way of drawing attention to code that will be fixed, but is not fixed yet.
It could look something like this:
# NOTE: This function does not work for negative numbers. # To be fixed. See issue #1234.
Starting with TODO Zero
So far so good, but how do you start using TODO Zero on an existing project which probably has TODOs in its codebase?
The answer is that you should get rid of every TODO it contains. That’ll involve some work – perhaps quite a bit of work – but I’ve found that it’s worth it. Although you can use TODO Zero without removing all the old TODOs, their continued presence causes confusion and I do not recommend it.
How you remove the TODOs depends on the project and the people working on it.
I don’t think there is any one right way, but here are some ideas:
Disregard every TODO.
On some projects, TODOs accumulate and are never dealt with. If this sounds like the project you’re working on, consider simply disregarding and deleting every one of them.
This is the quickest and easiest way. There may be some useful information lost if you do this though.
Create an issue for every TODO.
The polar opposite approach is to create an issue in the issue tracker for every TODO, then delete all the TODOs.
The problem with doing this is that it could clutter up your issue tracker with lots of low-priority tasks. The benefit is that it will make lots of lingering problems visible, so you can take action on them if you decide to.
There are automated tools like the todo Github App which might be useful if you take this path.
Go through each TODO and apply one of the four actions to it.
You could work through every TODO, applying one of the four actions to it and then deleting it.
The benefit of doing this is that every single TODO is dealt with thoroughly. The problem is that if there are any more than a small number of them it’s likely to be time-consuming.
Disregard every TODO created before a certain date, then deal with what’s left.
A hybrid approach is to delete all the TODOs which are ancient history without even looking at them, then work your way through those that remain. This could be a good approach if you have too many to handle any other way.
Ultimately though, what matters most is that all the TODOs are removed from the codebase.
Staying with TODO Zero
Once your project is using TODO Zero, any TODO which sneaks into your codebase will cause problems.
There are two factors to consider in guarding against this: people and automation.
It’s people who write and commit code, so you need to ensure that everyone on the project understands that there should be no TODOs in their finished work. Explain TODO Zero to them, and maybe suggest that they read this article.
Automation can also play a part by protecting the branch you ship from. Consider using a Git hook or a framework like pre-commit to warn or stop someone trying to merge code containing a TODO into a branch which shouldn’t have any.
Also, it’s worth occasionally grepping all the code for TODOs, just to weed out any which have found their way onto a branch they shouldn’t have.
Treat TODOs like sticky notes
The secret to using TODOs to write more robust code is to treat them like sticky notes.
Use them as reminders to yourself while you’re working, dispose of them as soon as they’ve served their purpose, and never let them end up on the branch you ship from!