Code readability is a big factor to that maintenance process. Code that is written well is easy and fast to read when problems arise. Same with infrastructure, it's never about just the initial build. Those structures need to be built with maintenance in mind: how to extend the longevity of a project to make the hours spent building it worthwhile. We need to care about the full lifecycle of any feature at every step.
Understanding how to reduce "visual" complexity is key, not just code complexity (although both, in my opinion, go hand-in-hand: code that has less cyclomatic complexity is often easier to read).
This article contains some general philosophies I've adopted with my approach to programming, specifically with how to better arrange logic statements. I am not the pioneer of these concepts and will link to other blog posts that I feel are more articulate about this than I am.
All examples are written with Python, but this applies to all programming languages with logic operators.
Sections
- Go completely "else"-less
- "Flip" logic statements to arrange by size of inner content
- Implement "getters" where possible over "setters"
- Try/Catch the smallest statement possible
Go completely "else"-less
Avoiding using else
statements entirely has been the most dramatic but effect change I've enacted to write better code. else
is convenient but masks the actual condition being hit before code is run.
Suppose I have the following code:
if condition_1:
print("🟦")
else:
if condition_2:
print("🟨")
else:
print("🟥")
What is the true logic that results in "🟥" getting printed? If that line was actually an exception thrown, how can I quickly understand this code in order to address the issue at hand? The example here is a smaller implementation of what might be more complicated logic routing and inner blocks of code.
Instead, consider firstly explicitly writing what each else
means, as in the negative of the previous if
statement:
if condition_1:
print("🟦")
if not condition_1:
if condition_2:
print("🟨")
if not condition_2:
print("🟥")
Being able to articulatly write out each else
may make it easier to find where guard clauses could be used, or where it would be more readable to combine the sub-conditions together. I am personally a fan of migrating this type of block into a function and returning with each if
statement hit:
def print_color() -> None:
if condition_1:
print("🟦")
return
if condition_2:
print("🟨")
return
print("🟥")
The only exception to else
would be for very small snippets of code where it is convenient to do so, such as Python's version of a ternary operator:
new_variable = "Value1" if condition_1 else "Value2"
Another example is if I need to shove a mapped value into a method where it makes sense for me to NOT keep the mapped value for other purposes:
run_weird_function(
input_arg = "Value1" if condition_1 else "Value2"
)
# Strs "Value1" and "Value2" are not referenced anywhere else in the code.
But in general, my recommendation is to move this to a function and ensure that the function name is straight-forward when read later throughout the codebase:
def get_variable() -> str:
if condition_1:
return "Value1"
return "Value2"
Recommended articles for further reading:
"Flip" logic statements to arrange by size of inner content
This goes hand-in-hand with the removal of ambiguous else
statements: the shortest amount of code should get run first to allow for the larger blocks of code to lie "flat" / run with less indentation. Guard clauses are a good example, where the "content" is typically a single line of code to raise an exception.
Take this code snippet for example:
for entry in full_list:
if condition_1:
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
else if condition_2:
print("🟨")
print("🟨")
print("🟨")
else:
raise RuntimeError("🟥")
In order for me to understand the else
here, I need to physically scroll far up the code and mentally negate the other two conditions. I use the indent-rainbow VSCode extension to help with this type of troubleshooting, but this feature requires me to load the code locally. GitHub's code viewer does not have a feature at this time to easily show what if
each else
is tied to.
The RuntimeError()
here should be migrated to the top as a proper guard clause that clearly states why it is getting run. I do this by "flipping" the logic: negate the condition and move it first.
Following it, the 3 "🟨" statements should be checked and run first before the 9 "🟦" statements. I am also a fan of adding a comment for any "implicit" logic statements being checked as a result of this re-arranging:
for entry in full_list:
if not condition_1 and not_condition_2:
raise RuntimeError("🟥")
if condition_2:
print("🟨")
print("🟨")
print("🟨")
continue
# if condition_1:
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
print("🟦")
continue
is my friend here for statements like this. But ideally, I would migrate any logic that needs to "exit early" into a separate function with proper return
statements.
Implement "getters" where possible over "setters"
In my experience, it is way more difficult to both unittest and debug a function if the output of the command is to modify an existing structure in-place. Instead, write individual getters for separate actions performed and then stitch them together into clearly-defined setters
For example, say I have an initial dict
and want to instead return a different dict
:
before_dict = { # Called "before_dict.json" in later example.
"00001": {
"date": "2025-01-01",
"user": "jdoe",
"title": "Lorem"
},
"00002": {
"date": "2025-01-02",
"user": "jsmith",
"title": "Ipsum"
}
}
# ... code goes here ...
after_dict = { # Called "after_dict.json" in later example.
"00001": {
"entry": "2025-01-01 | jdoe | Lorem"
},
"00002": {
"entry": "2025-01-02 | jsmith | Ipsum"
}
}
One strategy might be to write a method that modifies in-place the input dict
with the updated values:
def modify_input_dict(input_dict: dict) -> None:
for entry in input_dict:
input_dict[entry]["entry"] = " | ".join(
[
input_dict[entry]["date"],
input_dict[entry]["user"],
input_dict[entry]["title"]
]
)
del input_dict[entry]["date"]
del input_dict[entry]["user"]
del input_dict[entry]["title"]
While this code will perform the task at hand, the lack of rigidity of the structure makes it more confusing to understand what the data looks like during its execution. We might need to do extra checks in other parts of the code (i.e. to check for the existing of keys like date
) due to the lack of enforcement in this first upstream method. Another idea would be to not delete the keys in this example and to instead attach the entry
field to the dict
; but over time, that might cause this structure to become very large and filled with redundant fields.
Unit-testing with this approach also becomes a necessity: the stored mocks for the input and output would be useful as "documentation" for the modification being performed.
Instead, I recommend always writing methods that return and then using those as "setters" if absolutely necessary:
def get_entry(date: str, user: str, title: str) -> str:
return f"{date} | {user} | {title}"
def get_modified_input_dict(input_dict: dict) -> dict:
modified_dict = {}
for entry in input_dict:
modified_dict[entry] = {
"entry": get_entry(
input_dict[entry]["date"],
input_dict[entry]["user"],
input_dict[entry]["title"]
)
}
return modified_dict
The method cleanly defines the "before" and "after" in its own implementation. I understand the structure of output because I can see the whole thing here without needing to navigate to the unit-test fixtures. Writing unit-tests against this method is also easier: I can test the "actual" output against an "expected" output with a fed in "input" without needing to keep track of one or more variables for modifications:
def test_before() -> None:
"""Tests for the implementation where data is modified in-place."""
actual_output = get_mock("before_dict.json") # We confusingly name this "output" despite it being the input mock.
modify_input_dict(actual_output)
expected_output = get_mock("after_dict.json")
assert actual_output == expected_output
def test_after() -> None:
"""Tests for the implementation where getters are used instead."""
input_dict = get_mock("before_dict.json")
actual_output = get_modified_input_dict(input_dict)
expected_output = get_mock("after_dict.json")
assert actual_output == expected_output
Try/Catch the smallest statement possible
Suppose I have the following function:
def perform_task() -> None:
try:
value1 = get_variable()
value2 = get_variable()
func_might_throw_runtime_error(value1)
value3 = func_might_throw_assertion_error(value1)
func_might_throw_runtime_error(value2)
value3 = transform_values(value1, value2, value3)
func_might_throw_runtime_error(value3)
value3.sort()
return value3
except ...
If the logs return a RuntimeError, how do I know which statement here was the root cause? What if the message returned from func_might_throw_runtime_error()
is the same regardless of the input being sent in? Maybe we don't have control over this method, so we lack the ability to update the message with the specific erroring value.
At-a-glance, there is a lot going on to adequately drill into the problem without cross-referencing with the stack trace. We will eventually find the problem, but it will take extra, unnecessary lost time to get there.
Instead, each try/except should be set to wrap exactly the amount of code where the error can be encountered:
def perform_task() -> None:
value1 = get_variable()
value2 = get_variable()
try:
func_might_throw_runtime_error(value1)
except RuntimeError as runtime_error:
raise RuntimeError(f"Error: value1 = {value1}") from runtime_error
try:
value3 = func_might_throw_assertion_error(value1)
except AssertionError as assertion_error:
raise AssertionError(f"Error: value1 = {value1}") from assertion_error
try:
func_might_throw_runtime_error(value2)
except RuntimeError as runtime_error:
raise RuntimeError(f"Error: value2 = {value2}") from runtime_error
value3 = transform_values(value1, value2, value3)
try:
func_might_throw_runtime_error(value3)
except RuntimeError as runtime_error:
raise RuntimeError(f"Error: value3 = {value3}") from runtime_error
value3.sort()
return value3
The above snippet serves as an example of what might be seen in practice. The resulting method is significantly larger but is much more useful when needing to drill into issues. We can clearly map each inner function called to the outer error message in our logs without the stack trace.
We may also be making incorrect assumptions around the behaviors of the other methods present here too. What if transform_values()
also returns a RuntimeError with certain inputs? We can save ourselves so much heartache and debugging hours by updating our code to try/except wrap each method; this will surface these assumptions when they're encountered properly and allow us to go in and wrap those assumptions with better handling.