BYU logo Computer Science

Quality Code

When you write code, you want to write code that is easy to read, understand, debug, and maintain. In this section, we will cover our standards for writing quality code in Python. These standards include intent, format, decomposition, and style.

Intent

Each assignment has an intent; we create the assignments so you can learn how to use the tools we give you in specific ways. Most assignments will state this intent in the grading section of each assignment. If not explicitly stated, the intent is that you will use the topic of the day in a way consistent with what you learned in lecture and lab.

A common intent for your assignments is generality, meaning that your code should be able to solve problems that all have the same requirements (for example, solve two different Bit worlds that have the same general qualities).

When your solution takes advantage of a particular quirk of the input not stated in the problem, we call that “hardcoding”. In this case, your code can only solve one very specific problem.

Many of the assignments in Unit 1, including some of the project problems, require fully hardcoded solutions. However, in Unit 2 and beyond, we expect general solutions.

Your solution can assume a particular input format (i.e. the test will always input a number when a number is expected); however, your code should not assume a specific input value (i.e. the bit world will be 5 columns wide, or the author will always be “Charlotte Bronte”).

Run the PyCharm Code Formatter

Before you turn in a submission, we expect you to format your code. You may have had experience before with MLA format when writing essays; there are similar style guidelines for writing code. Instead of listing out all of the requirements, we will let PyCharm format your code for you.

In PyCharm you can format your code by pressing Ctrl + Alt + L on Windows or Cmd + Option + L on Mac. You can also find a button under Code -> Reformat Code.

The formatter will:

  • Use 4 spaces for indentation
  • Put 2 blank lines between functions
  • Put spaces around operators
  • Remove unecessary indentation
  • Wrap extra long lines onto new lines

Decomposition

Decomposition is the process of breaking your code into smaller, more manageable pieces. When you decompose your code, you make it easier to read, understand, debug, and maintain.

Use Functions to Summarize a Sequence of Steps

Break your code into functions. If the problem asks for a sequence of distinct steps, use a function for each step.

# Good
def run(bit):
    get_to_garden(bit)
    paint_garden(bit)
    go_to_end(bit)

Use Functions to Represent Repeated Ideas

Avoid duplicate code. When two sections of code are identical (they call the same functions in the same order), that code is duplicated. You can avoid duplicated code by using functions. Put the repeated code in a function. Then replace each duplicated code sections with a call to the new function.

The following examples paint a rectangular world blue, as is shown in the blue ocean guide.

# Good decomposition
def half_loop(bit):
    bit.turn_left()
    while bit.can_move_front():
        bit.move()
        bit.paint("blue")
    bit.turn_left()


def blue_column(bit):
    half_loop(bit)
    half_loop(bit)
    

def blue_ocean(bit):
    blue_column(bit)
    while bit.can_move_front():
        bit.move()      # Glue code
        blue_column(bit)
        

# Good decomposition
def turn_around(bit):
    bit.turn_left()
    bit.turn_left()


def blue_column(bit):
    bit.turn_left()
    bit.paint('blue')
    while bit.can_move_front():
        bit.move()
        bit.paint('blue')
    turn_around(bit)
    while bit.can_move_front():
        bit.move()
    bit.turn_left()
    

def blue_ocean(bit):
    blue_column(bit)
    while bit.can_move_front():
        bit.move()        # Glue code
        blue_column(bit)

        
# Bad, duplicate code
def turn_around(bit):
    bit.turn_right()
    bit.turn_right()


def paint_first_column_blue(bit):
    bit.turn_left()
    bit.paint('blue')
    while bit.can_move_front():
        bit.move()
        bit.paint('blue')
    turn_around(bit)
    while bit.can_move_front():
        bit.move()
    bit.turn_left()


def paint_column_blue(bit):
    bit.move()
    bit.turn_left()
    bit.paint('blue')
    while bit.can_move_front():
        bit.move()
        bit.paint('blue')
    turn_around(bit)
    while bit.can_move_front():
        bit.move()
    bit.turn_left()


def blue_ocean(bit):
    paint_first_column_blue(bit)
    while bit.can_move_front():
        paint_column_blue(bit) # No glue code

        
# Very Bad, one function
def blue_ocean(bit):
    while bit.can_move_front():   
        bit.turn_left()
        bit.paint('blue')
        while bit.can_move_front():
            bit.move()
            bit.paint('blue')
        turn_around(bit)
        while bit.can_move_front():
            bit.move()
        bit.turn_left()
        bit.move()
    
    bit.turn_left()
    bit.paint('blue')
    while bit.can_move_front():
        bit.move()
        bit.paint('blue')
    turn_around(bit)
    while bit.can_move_front():
        bit.move()
    bit.turn_left()

The good examples use functions to reduce duplicate code. Some repeated glue code is ok. The ‘Very Bad’ example does not uses functions.

The ‘Bad’ example has two functions that do nearly the same thing. To avoid this, use glue code. Glue code is purposefully outside a function, enabling varying behavior before or after a function call. In the ‘Bad’ example, paint_column_blue() is nearly the same as paint_first_column_blue(), but it has an extra bit.move(). To decompose this problem well, the bit.move() should be outside the function. To see the bit.move() used as glue code, take a look at both ‘Good’ examples.

In Units 1 and 2, if you have two nearly identical functions, put the duplicated code in one function, and use glue code to change the behavior before and after the function call.

Look for opportunities to combine similar functions (Unit 3 onward)

Unit 3 onward, if you have two similar functions that perform a similar task, combine them into a single function that uses a parameter to control the difference.

# Good, general function
def get_list(prompt):
    items = []
    while True:
        response = input(prompt)
        if response = '':
            break
        items.append(response)
    return items
    

def main():
    renters = get_list("Renter: ")
    places_to_rent = get_list("Rental Property: ")



# Bad, nearly identical code in two functions
def get_renters():
    renters = []
    while True:
        name = input("Renter: ")
        if name = '':
            break
        renters.append(name)
    return renters


def get_addresses():
    addresses = []
    while True:
        address = input("Rental Property: ")
        if address = '':
            break
        addresses.append(address)
    return addresses


def main():
    renters = get_renters()
    places_to_rent = get_addresses()

The good examples use functions to reduce duplicate code. Some repeated glue code is ok. The ‘Very Bad’ example does not uses functions.

Use Functions to Avoid Deep Indentation

Code with several levels of indentation is hard to read. When one block of code is indented to the right of another code block, we call it “nested”. We might use the phrase “highly nested code” to describe a function that has too many layers of indentation, or the term “nested while loop” to mean a while loop inside another while loop. You can reduce indentation using functions.

In this class, the problems are simple enough that you do not need more than 2 levels of indentation. We expect you reduce your indentation to 2 levels using functions. In later classes, you may need additional levels of indentation.

The following code example moves bit from left to right. When bit encounters a green square, it paints green until hitting a red square.

# Good, maximum indentation is 2
def paint_line(bit):
    # Level 0
    while not bit.is_red():
        # Level 1
        bit.paint("green")
        bit.move()
    

def traverse_row(bit):
    # Level 0
    while bit.front_clear():
        # Level 1
        bit.move()
        if bit.is_green():
            # Level 2
            paint_line(bit)



# Bad, 3 levels of indentation
def traverse_row(bit):
    # Level 0
    while bit.front_clear(): 
        # Level 1
        bit.move()
        if bit.is_green():
            # Level 2
            while not bit.is_red():
                # Level 3
                bit.paint("green")
                bit.move()

The bad example has all of the code in one function, so the maximum indentation is 3. The good example moves the nested while loop to a new function to reduce the indentation to 2.

Imports

  • Import modules at the top of your file.
# Good, import at the top
import math


def main():
    pass


# Bad, import after a function
def main():
    pass

import math


# REALLY BAD, import inside a function
def main():
    import math

In this class, we will import modules at the top of the file. However, in some rare cases, you may see imports inside functions, but this is outside the scope of this class.

if __name__ == '__main__':

  • If you want your code to be run as a python program, include if __name__ == '__main__' and call your primary (main) function from there.
  • Put the code you want to run in the if __name__ == '__main__': block.
# Good, code is inside functions
def calculate_area_of_circle(radius):
    return 3.14 * radius ** 2


def main():  # This is the primary function, not required to be called main
    radius = 5
    area = calculate_area_of_circle(radius)
    print(area)


if __name__ == '__main__':  # We call this the main block
    main()


# Bad, code is outside functions
def calculate_area_of_circle(radius):
    return 3.14 * radius ** 2


radius = 5
area = calculate_area_of_circle(radius)
print(area)


# REALLY BAD, all code is in the main block
if __name__ == '__main__':
    def calculate_area_of_circle(radius):
        return 3.14 * radius ** 2

    radius = 5
    area = calculate_area_of_circle(radius)
    print(area)

Style

In this section, we will cover our style conventions for writing Python code. These conventions are important because they help you write code that is easy to read, understand, debug and maintain. Aspects of style that we will cover include: naming conventions and whitespace.

Naming

Case

The style of variable names depends on the programming language. In python, snake case is used for function and variable names.

Snake case is when you use lowercase letters and separate words with underscores. For example:

# Snake case 
pie_flavor = 'key lime'

def get_flavors():
    pass

Camel case is when you use lowercase letters for the first word and uppercase letters for the first letter of each subsequent word. It is often used in Java and JavaScript. For example:

# Camel case
pieFlavor = 'key lime'

def pieFlavor():
    pass

In this class, since we are using Python, we will use snake case for variables and functions.

Variables

  • Use snake case for variable names.
  • Be descriptive with variable names.
  • Short names are fine, to an extent.
# Good
first_name = 'Emma'
last_name = 'Smith'
small_number = 5
small_num = 5
pokemon_type = 'water'
poke_type = 'water'

# Bad
name = 'Emma'
otherName = 'Smith'
num = 5
typ = 'water'

# REALY BAD
thing = 'Emma'
this = 'Smith'
that = 5
other = 'water'

In the example above, the good example uses snake case for variable names and is descriptive. The bad example uses camel case and vague variable names. While the really bad example uses non-descriptive variable names.

Functions

  • Use snake case for function names and function arguments.
  • Be descriptive with function names and function arguments.
  • Short names are good, to an extent.
# Good
def calculate_area_of_circle(radius):
    ...

def calc_circle_area(radius):
    ...

# Bad
def calculateAreaOfCircle(Num):
    ...

def cir_ar(num):
    ...

# REALLY BAD
def process(num):
    ...
    
def doThing(foo):
    ...

In the example above, the good example uses snake case for function names and arguments and is descriptive. The bad example uses camel case for function names and vague function arguments. The really bad example uses non-descriptive function names and arguments.

General Example

import sys


def get_items(how_many, prompt):
    items = []
    while len(items) < how_many:
        item = input(prompt + ': ')
        items.append(item)
    return items


def display_items(items):
    for item in items:
        print(f'- {item}')
        
        
def main(how_many, prompt):
    items = get_items(how_many, prompt)
    display_items(items)


if __name__ == "__main__":
    how_many = int(sys.argv[1])
    prompt = sys.argv[2]
    main(how_many, prompt)

Notice the decomposition of this code. It is broken into functions with only one level of indentation. The if __name__ == '__main__': block contains all references to system arguments, as well as a call to the primary function. This makes the code easier to read, understand, debug, and maintain.

Also notice the style. The variable, function, and argument names are snake case and descriptive. The import is at the top of the file.