Most of you have probably written a workaround for some bug in a web API or a third-party package. If you haven't, trust me, it's just a matter of time. The question then becomes, how can I make sure the fix isn't pre-maturely removed or overstays its welcome? I aim to give you some tools I have found useful when writing workarounds. And if you have any good tips, please add them in the comments!
Our Example
First off, I want to set up a fictive example to compare against. This is a class of a third-party library, finder_lib
, I am using:
class Finder:
def __init__(self, pattern):
self.pattern = pattern
def find(self, text):
if self.ignore_case:
text= text.lower()
return text.find(self.pattern)
Here we have a class that takes a pattern when instantiating the object. You can then use the finder object to find that pattern in multiple strings. However, we have a bug! When calling the find
method it will crash because self.ignore_case
isn't defined. This value should be set when creating the object, but we can't as the __init__
method doesn't take it as a parameter. Yes, this is a very simple case but it is based on a workaround I had to write once. While that class was more complex, the bug is structurally the same. It also had a required field that wasn't accepted in the __init__
method.
Python is a very dynamic language that allows us to easily work around this issue:
from finder_lib import Finder
finder = Finder("hello")
finder.ignore_case = True
finder.find("Hello World")
Now we have a bug and a way to work around it. Time to get into the main part of this post, how to manage this workaround during its lifetime.
Extract the Workaround
Making sure the workaround is written only once goes a long way. It makes it sooo much easier to remove when the day comes. In this case, I would extract it to a function:
from finder_lib import Finder
def create_finder(pattern, ignore_case=False):
finder = Finder(pattern)
finder.ignore_case = ignore_case
return finder
finder = create_finder("hello", True)
finder.find("Hello World")
Remember that we have a lot of tools in the Python arsenal. If the workaround is complex we may use modules or classes to break it up. If it has set-up and tear-down, maybe we should use a context manager. Maybe a decorator can be used to make sure it always runs before or after the required functions. In some cases, even monkey patching might be the best option. What tools to use will depend on the nature of the workaround. Use your best judgment and you will be fine.
Comments
For workarounds, code comments are a very useful tool. I don't think the most avid opponents of code comments would argue against their use here. You see, workarounds often require a lot more context to be understood. They are, almost by definition, breaking the ordinary programming structures, hierarchies, or flows of our application code.
Let's start with an example of how I write my code comments for workarounds and then I'll break it down.
from finder_lib import Finder
def create_finder(pattern, ignore_case=False):
finder = Finder(pattern)
# FIXME: Workaround, missing ignore_case parameter in __init__ [2020-06-16]
# Implemented for version 2.1.12 of finder_lib.
# This is a known issue documented at https://github.com/finderlibteam/finderlib/issues/1234
finder.ignore_case = ignore_case
return finder
finder = create_finder("hello", True)
finder.find("Hello World")
It's so common to use prefixes such as "fixme" or "todo" in code comments that many editors have built-in support or plug-ins for finding them. In VS Code FIXME is highlighted (I think by default) and I use the plug-in Todo Tree to get a list of all these comments in the codebase.
One distinct part of these comments is that I always add the date in brackets at the end of the first line. You could argue that source control will show when the workaround was written. You would be correct, however, I find that the date pulls my attention to the comment. Especially if the date was a long time ago. My thought is that others will react to old dates the same way. What happened here two years ago? Hopefully this will trigger them to look into if the workaround can be removed. You see, my goal is to get the workaround expelled from code as soon as possible. For the same reason, I think it is a good idea to add information like the library version. If I know we are on 3.4.0, a fix for 2.1.12 might very well be out of date.
If there isn't an issue in the library's issue tracker, you should preferably create one. When you have found the issue or reported it, I would recommend adding the link in the code comments. This makes it easier to check in on the progress next time you scroll past this piece of code.
Some final notes on comments. In cases where the workaround is more complicated than this example, add more context. Maybe describe in more detail what the issue is and how the workaround aims to solve it. If there is a planned or expected version for the bugfix, write down this version as well. Context is everything when you try to understand strange code.
Sidenote: You might be screaming at me for not using doc-strings here. In general, I would add a doc-string as well with some of this information. However, I would keep the comments as they are more likely to be picked up by todo-finding tools.
Testing
A helpful method for detecting if a workaround has served its purpose is to add a test that will fail when the fix can be removed. This might not always be possible, but if you can it's nice to have. For our example a test could look like this:
from finder_lib import Finder
def test_Finder_workaround_can_be_removed():
error_was_raised = False
try:
Finder("hello", ignore_case=True)
except TypeError:
error_was_raised = True
msg = "Finder accepts ignore_case, remove workaround in mymodule.create_finder"
assert error_was_raised, msg
This test will be detected by pytest
, a common third-party testing framework, and will pass as long as Finder
doesn't accept ignore_case
. We detect this by looking for a TypeError
, as this exception is raised when passing an unsupported parameter to a function or class in Python. As long as the error has been raised the workaround is still valid.
Summary
When you have written a workaround your main goal is that it should have as little impact on the application code as possible. This means that we want the impact on the code to be small, have the workaround removed as soon as possible, and for everyone to understand why the code looks strange. We can do this by extracting the workaround, for example into a function, minimizing its footprint in the application code. We should also take extra care to document it thoroughly. The documentation should make sure everyone understands when it was created, why it is needed, and when it can be removed. Finally, in some cases, we can also write a test that fails as soon as the workaround can be removed. Having a test makes it automatic, which is great.
Final notes
I hope you found this post helpful and can use some of these techniques for your next workaround. If you have any tips of your own, please add a comment below, I would like to hear how you have approached this in your own work. Happy coding!😄