All Dangers of Side Effects for Python Coders
Oleg Sinavski
Posted on July 4, 2023
Are there enough posts about unnecessary side effects? Unlikely - you still see them everywhere.
Here is a sales pitch for this post:
- no functional programming jargon
- a go-to list of reasons to avoid side effects
- Python-oriented
Spotting side effects
Some posts describe side effects in the context of functional programming (and JavaScript): one, two, three and a wiki. You don't have to study functional programming to grasp this concept. Unfortunately, unnecessary side effects are prevalent in all coding paradigms, especially OOP.
Let’s take a simple function that computes the absolute value of a number:
def abs(x):
if x < 0:
return -x
return x
What comes out from the return
statement is called a “primary effect”. Now let’s take a look at another function:
def abs_effectful(x):
if x < 0:
return -x
twitter_api = TwitterAPI(some_twitter_id())
chat_bot = LanguageModel()
tweet_content = twitter_api.read_first_tweet()
response = chat_bot.make_response(tweet_content,
f"{x} is important because ")
twitter_api.respond(response)
twitter_api.close()
return x
Here we also have the same primary effect returned by the abs_effectful function. But a section in the middle performs pretty surprising actions and influences the external world: it has side effects.
You may ask: "Why do you have a weird unrelated piece of code in your function??" This contrived example is not so far from what happens in large codebases. It’s just a set of refactorings, misunderstandings, and a stream of people joining and leaving teams. Time passes, and simple code accumulates unnecessary baggage.
Here are some real examples of side effects that I've encountered in my Python ML career:
- publishing a number to the Weights-and-Biases server out of the loss function
- dumping debug images into a home folder from a computer vision algorithm
- changing an environmental variable inside a function
- dropping to
pdb
onif
condition - changing a global variable
- calling
sys.exit
inside an algorithm - sending a Slack message if
nan
is encountered during training - ... and so on.
After seeing many of those, you get a Spider-Sense for spotting side effects. If you get surprised by what function does, quite often the function has a side effect. In fact, the code with unnecessary side effects is not as simple as it could be. See a great post from Eric Elliot about simplicity, surprises and side effects.
Side effects are unavoidable because software must have some influence on the world. But are unnecessary side effects harmful? If there are no visible differences, is there a reason to prefer one script over another?
Unexpected crashes
How can abs
crash? It pretty much can’t. What about abs_effectful
? It might crash in almost every line:
- a Twitter API might fail to initialize
- a language model can't allocate GPU memory
- it might fail to parse or generate a phrase
- a Twitter API might fail to publish a response or crash upon closing
Every side-effect code line might crash since it deals with things you don't control. Now, from the real examples above:
- Weights-and-Biases or Slack might be unavailable
- you might run out of space in the home folder
- an environmental variable is deleted before setting, and so on ...
So the code with side effects is more fragile.
Unexpectedly slow (and occasionally very slow) code
Every side effect might bring computational costs. But most importantly, it brings timing variability into your software. Because side-effect code typically deals with uncontrolled external resources, you get uncontrolled slowdowns. Every side-effect code line in abs_effectful
can bring you a massive slowdown:
- a Twitter API could be slow to connect
- a language model might wait for free GPU memory
- computational load on the system might make phrase parsing too slow
In real-world code, similar things can happen:
- database access could be very slow because the network is busy
- writing to a hard drive could be slow because of fragmentation
- logging could be too slow because other loggers compete for file access, and so on ...
The bottom line is that the code with side effects is slower.
Hard to use it concurrently
You must be careful about any state you modify in concurrent (or even worse parallel) software to avoid data races and deadlocks. Code with side effects modifies the external state. It will often require a significant redesign to be used in a concurrent context. You need to ensure that only one function will use a global resource simultaneously and that the order of operations doesn't break the logic.
The timing variability mentioned above makes it even worse.
It's a big topic, so here is a good post discussing concurrency and shared state access in depth.
Hard to optimize
Optimizing a function that doesn't modify anything externally is more straightforward. You can reorder operations inside it and cache the computation results. Since you're not constrained by an external call for a side effect, you can choose any tool for the job. Many side-effect-free functions can be easily rewritten in numpy
/torch
/jax
and Pybind
.
So on top of being slow, code with side effects is hard to speed up.
Hard to understand
What if I ask you to make a docstring for abs_effectful
? You will definitely struggle to write a concise story about the purpose of that function.
A function with a single return statement like abs
achieves a single result. At a minimum, you can concisely write a docstring as
'''This function computes an absolute value.'''
If there are side effects, you have to add them all one by one:
'''This function computes an absolute value
AND opens Twitter API and creates a model
AND generates and sends a tweet
AND closes connections.
'''
In a large company, many people are joining and changing teams. New people have to understand obscure side effects. A company with less-readable code will waste days of cumulative onboarding time. A large docstring will not help: the longer the doc, the greater the chance it will be ignored completely.
Hard to release and debug
The code with side effects interacts with stuff which is not under your direct control. After thinking about all imaginable failure modes of abs_effectful
, you might still find that it crashes after the release.
There is a crazy variability in the external world. It is tough to reproduce, debug and fix all edge cases of side effects. Often it happens stochastically, and you just can't reproduce a single crash in-house (e.g., maybe when abs_effectful
runs on an old Windows version, GPU kernels behave differently?).
The code with side effects takes much longer to release and debug.
Hard to test
Writing exhaustive unit tests for code with side effects is very hard. On top of the main result of abs_stateful
, you should better test that all side effects behave as expected: did the person get a tweet? Was the tweet parsed correctly? Not to say of some weird unit test interactions: side effects from one test could make other tests fail.
Interacting with the external world on a test server is often impossible. Developers have to resort to mocking all external entities. See more in this beautiful post from CodeWhisperer.
Hard to reuse
What if your colleague wants to compute the absolute value of a number? There is no problem with abs
. You just import the function and use it! What about abs_effectful
? They wouldn't know a priori about any side effects (ignoring the hint in the name :). The code can happily work until some unfortunate day when some crazy issues from above start coming up.
After finding the root cause, they'll anticipate all the problems we mentioned. Your colleague will just leave such a function alone and write their own simpler version.
A codebase that relies on side effects would typically have much less code reuse.
Conclusion
Hopefully, you’re convinced that side effects are also the root of all evil (just like premature optimization). Now here is a compilation of all the reasons above:
Code with side effects
- is fragile - leads to unexpected crashes
- is unexpectedly code and is hard to optimize
- is hard to read and understand, it is hard to reuse
- is hard to debug and write tests
- is hard to use concurrently.
For more discussion, please see the following great thread on stackexchange.
Meanwhile, thank you for reading!
Posted on July 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.