5 Critical Steps to Improve Your Code Quality
Well-written code is essential to the longevity and scalability of software. There are major advantages to be gained by thinking about how to write better code.
Every software developer will at some point run into a situation where they have hundreds or thousands of lines of code to look through only to be left staring glassy-eyed at a confusing mess of spaghetti code.
Just like in creative or academic writing, HOW you write is just as important as WHAT you write. Software development is rarely a solo sport, therefore it's crucial that everyone can understand what’s going on in the codebase at a glance.
And so you’ve come before me, brazen and unrelenting in the eyes of the software gods. Pick up your digital quill and prepare yourself, nomad. Your epic journey into the world of code quality begins now.
With that said, here are 5 steps to improve your code quality today!
1. Document your code correctly
This is commonly understood to be one of the starting points for writing quality code. This is also something you can start practicing today.
Documentation involves simple things like comments and docstrings within your code as well as external documentation and diagrams outside your code.
By documenting your code, you can offer key insight for others — and your future self — to better understand the purpose of each moving part of the codebase.
Comments & Docstrings
Comments are best reserved for crucial elements that need a little more explanation. The goal is to be slightly conservative on how and when you use comments, as writing too many can often have a negative effect.
For example, elements such as functions, classes, and certain sections can benefit from comments to better explain their purpose in the bigger picture.
Whereas writing comments for every new variable you initialize just ends up adding visual clutter. A good rule to follow is if a comment adds no positive value or makes the code more difficult to understand, it’s not worth it.
First, consider the following snippet:
# Check if the user account is frozen and deny or grant platform access.
if user.isFrozen() == True:
denyAccess()
else:
grantAccess()
In the above example, we add a comment that gives subtle information to the reader that would have otherwise been lost.
We could tell by the context and names that this chain has to do with users and credentials but without the comment, we would have never known that it deals with account status and admin credentials.
This is a good way to think about it as well. If something has a hidden meaning or nuance, make sure to clearly document that to prevent any confusion.
One of the other uses of comments is for defining docstrings on things like utility functions and classes. You may have seen this in use within the IntelliSense tooltip of a random function.
Docstrings are comments that give readers a structured description of the function’s inputs, outputs, and general purpose. Below we have an example of a docstring for the hasDuplicates
function:
def hasDuplicates(string, char):
"""
Check if a string contains multiple occurrences of a character.
This function takes a string and character as input and
returns a boolean value. The return value reflects whether
or not the string contains more than 1 occurrence of the char.
Args:
string (str): The string to be checked for duplicates.
char (str): The character to compare for duplicates.
Returns:
bool: True if the string contains 1 or less occurrences of the char.
False otherwise.
Example:
>>> hasDuplicates( my..file.txt, "." )
True
>>> hasDuplicates( "image.jpg", "." )
False
"""
count = 0
for c in string:
if c == char:
count += 1
if count >= 2:
return True
else:
return False
There are many different types of structures for docstrings but in this example, we simply included:
- A short description of what the function does.
- A longer description that goes into more detail.
- The arguments the function accepts.
- The return value of the function.
- An example of the function in use.
Keep in mind the naming of the function as well. Here we write hasDuplicates
in camelcase, which tells the reader at a glance that this function checks if something has any duplicates. Even without the docstring, you can get an idea of what the purpose of the function is at a glance.
Naming is not an exact science and is largely up to you how you choose to name things. What matters most is that you remain consistent in your naming conventions to ensure clearer documentation. Speaking of naming conventions…
2. Use naming conventions the right way
How you name things, such as functions, variables, and classes, is very important in improving the quality of your code.
Generally, we want to aim for two things in our naming conventions: clarity and consistency.
Clarity
Clarity means to name things in such a way that even a complete stranger could understand what something in your codebase does.
For example, a variable named something generic such as clr_1
is difficult to understand. Instead, we could change it to something likecolor_primary
to represent the primary color in a set of colors or border_color
for the color of a border. Keep it short and precise.
Consistency
Consistency means to use the same conventions and guidelines throughout your code. This could be something as simple as not changing the case of your functions randomly, i.e. is_end
and isStart
.
It could also mean using similar affixes for related variables, i.e. car_color
, car_width
, and car_height
instead of just c
, w
, and h
.
By focusing on how you name things, you can also make the process of writing code much faster because everything has an established convention from the start.
Just don’t spend hours trying to get the perfect name for a function, many a developer has fallen prey to such an endeavor.
3. Break your code down into smaller parts
One of the first concepts you will have learned in software development and computer science is Abstraction. As a refresher, abstraction refers to the simplification of complexities in order to focus on only the most important information.
Applying abstraction
In this context, we can apply the concept of abstraction by simplifying our codebase into clear, understandable blocks, i.e. breaking it down.
In the most general sense, this means instead of writing a few giant functions you should aim to break them down into smaller, bite-sized functions.
Both during your planning and development stages, consider all of the most important moving parts and how each can be broken down into its own function or class. Just remember, as with all things, we don’t want to go overboard and break everything down into hundreds of little tiny parts.
First, consider the following snippet:
def main():
if user_input != None:
process_source_file(source_file, new_file, "both")
else:
handle_invalid_input(user_input)
def process_source_file(source_file: Path, new_file: Optional[str], format: FormatTypes) -> None:
"""Process the passed source file and prepare it for conversion."""
if not source_file.exists() or not source_file.is_file() or source_file.suffix != ".json":
handle_invalid_source_file(source_file)
return
if new_file is None:
new_file = choose_new_file_name(source_file)
if utils.has_duplicates(new_file, "."):
handle_invalid_new_file(new_file)
return
format_and_save(source_file, new_file, format)
def handle_invalid_source_file(source_file: Path) -> None:
"""Handle the case of an invalid source file."""
err_console.print(Panel(
f"The path {source_file} is not a valid JSON file. Please check the path and try again.",
title="Error",
title_align="left",
border_style="red"
))
In the above example — which I have cut down for clarity — we see this concept of breaking things down into smaller pieces in action.
We could have included the two function’s code in just the main()
function but that would have been less modular, more difficult to differentiate, and more difficult to scale.
There are only two functions shown in this example but what if we were to have ten, twenty, or even thirty functions? The main()
function would quickly become a mess and even with descriptive comments, it would be hard to tell each function from one another.
By breaking each step/process into its own function, we can tell at a glance what each part is doing and where to go to modify that particular part should we need to adjust or debug it.
Imagine if every part in your car looked the same and had no name associated with it. In this way, it helps to think of your code as the underlying gears that power your mechanism allowing it to operate at peak efficiency. So grab your wrench and break down the — alright, enough of the analogies, let’s move on.
4. Don’t repeat yourself
Lovingly referred to as DRY by some developers, this principle states that you should never repeat yourself or write redundant code.
This benefits our goal of writing quality code because a clean and tidy codebase is a happy codebase — or at least the closest approximation for a computer.
To apply this principle, you will want to actively cut down on any code that is needlessly repeated. It can be hard to see this in your code at first but keep an eye out and it will get easier to notice over time.
One of the first cases of this you will notice is with iterative functionality and statements such as loops. To clarify, consider the example below:
# Before - No function for loops
def main():
input = "Hello World!"
msg = "Hello Human!"
isEnding = True
# Loop 1
for char in input:
cprint(char, color, end="")
sys.stdout.flush()
time.sleep(delay)
if isEnding == True:
print("")
else:
return
# Loop 2
for char in msg:
cprint(char, color, end="")
sys.stdout.flush()
time.sleep(delay)
if isEnding == True:
print("")
else:
return
# After - reusable function for loops
def main():
input = "Hello World!"
msg = "Hello Human!"
printTextStaggered(input, 0.3, "red", True)
printTextStaggered(msg, 0.4, "blue", False)
def printTextStaggered(str, delay, color=None, isEnding=False):
for char in str:
cprint(char, color, end="")
sys.stdout.flush()
time.sleep(delay)
if isEnding == True:
print("")
else:
return
In the above example, the before section was an example of redundant code that we simply copied and pasted with only a few adjustments. This is bad for a few reasons:
- It takes up more lines of code, adding bloat to the overall codebase.
- It reduces the overall mutability of these pieces of code.
- It makes it difficult to debug on both a macro and micro level.
In the after section, however, we cut out that loop into its own function called printTextStaggered()
. With this, we can now reuse it simply by calling it and passing in new arguments, all on one line.
This helps with scalability and any sweeping changes we might need to make. Adding changes to the before section would require that we find every instance of what we wanted to change in each loop and do it manually, potentially over dozens of lines.
In the after section, it would be as easy as changing the arguments on each line where we call the function.
The takeaway here is to look for instances in your code where you can bundle some functionality that is needlessly repeated into a reusable function or a class. This could be through using enums, objects, methods, or any other tool that can help make your functionality as reusable as possible.
5. Structure your files and content
Lastly, a simple but often forgotten step in writing quality code is to make your files and content follow a clear and readable structure. This touches on everything we’ve discussed so far and ties it all together.
When I say structure, I am referring to a multitude of things, but for now, let’s focus on the content structure of your files.
Take a look at this code:
pathlib import Path
from enum import Enum
from utils import file_utils as utils
import typer
from typing_extensions import Annotated
from typing import Optional
from rich.console import Console
from rich.panel import Panel
app = typer.Typer()
err_console = Console(stderr=True)
notice_console = Console()
DEF_EXT = ".txt"
FALLBACK_FILE_NAME = "converted_file"
class FormatTypes(Enum):
properties: str = "properties"
values: str = "values"
both: str = "both"
@app.command()
def main(source_file: Annotated[Path, typer.Argument(exists=True, readable=True,file_okay=True,dir_okay=False,help="The path of the source file to convert. Must be a valid directory or file with extension .json.")):
Though exaggerated, you may be able to tell how confusing and tense this code feels. It seems cramped and it’s hard to tell where one thing ends and something new begins.
Now, take a look at this code:
from enum import Enum
from pathlib import Path
from typing import Optional
import typer
from typing_extensions import Annotated
from rich.console import Console
from rich.panel import Panel
# Module imports
from utils import file_utils as utils
# Typer Declerations
app = typer.Typer()
err_console = Console(stderr=True)
notice_console = Console()
# Global Variables
DEF_EXT = ".txt"
FALLBACK_FILE_NAME = "converted_file"
# Classes & Enums
class FormatTypes(Enum):
properties: str = "properties"
values: str = "values"
both: str = "both"
@app.command()
def main(
source_file: Annotated
[
Path,
typer.Argument
(
exists=True,
readable=True,
file_okay=True,
dir_okay=False,
help="The path of the source file to convert.
Must be a valid file with extension .json."
)
],
):
With this example, you should be able to tell just how open it feels. Now you can clearly see how each section of content is set up and the flow from the top-level content, such as imports and global declarations, to the meat of the software found in the main() and supporting functions to come.
What we want to consider is how to make our code easier to read for humans by utilizing things such as whitespace, comments, tabs, and more. Of course, you must adhere to the syntax requirements of your language but when possible try to block off related sections using intentional structural formatting.
As you write code, try to tell a story that includes a solid beginning, middle, and end. In doing so, you will get one step closer to achieving quality code.
Final thoughts
Today we learned some solid steps for improving the quality and readability of your code — or at least some nice food for thought. Even if you know these concepts already it’s beneficial to review them every once in a while to gain a new perspective on them.
In summary:
- Document your code
- Use naming conventions
- Break your code down
- Dont repeat yourself
- Structure your content
As always, I wish you the best, keep on learning new things, and I’m sure that you will reach your goals. Feel free to leave any comments and questions.
Until next time.
🧠 Follow me on YouTube for business tips, coding tutorials, and more!