Table of Contents |
---|
...
Introduction
...
...
...
...
...
...
Object-Oriented Programming (OOP) Coding Standards
...
...
OOP Abstract Base Class (ABC) Coding Standards
...
Introduction
The Sibros Python coding standards serves as a general programming guideline for a variety of aspects such as style guide, conventions, and design. The guidelines extend beyond Python's PEP 8 style guide, and just like PEP 8, the guidelines only serve as recommendations.
Why Python?
Python is an excellent programming language that has multiple advantages over other languages.
Easy to learn
Python was designed to be written closer to English by simplifying the overall syntax; it generally takes less lines of code to achieve similar functionality in other languages like C or C++.
Great for prototyping
Because of Python's simplistic nature, it has a quick turnaround time to create programs for a wide range of applications ranging from internal/external standalone tools to simple scripts for automation.
Object-oriented
Python is an object-oriented language, so developers can take advantage of object-oriented design to create complex yet elegant designs.
General Philosophy
Readability vs. performance
The Sibros Python coding standards serves as a general programming guideline for a variety of aspects such as style guide, conventions, and design. The guidelines extend beyond Python's PEP 8 style guide, and just like PEP 8, the guidelines only serve as recommendations.
...
Why Python?
Python is an excellent programming language that has multiple advantages over other languages.
Easy to learn
Python was designed to be written closer to English by simplifying the overall syntax; it generally takes less lines of code to achieve similar functionality in other languages like C or C++.
Great for prototyping
Because of Python's simplistic nature, it has a quick turnaround time to create programs for a wide range of applications ranging from internal/external standalone tools to simple scripts for automation.
Object-oriented
Python is an object-oriented language, so developers can take advantage of object-oriented design to create complex yet elegant designs.
...
General Philosophy
Readability vs. performance
Python should be written with readability as the utmost priority; optimization is secondary. Optimization should only be done if the problem being solved is proven to be a problem in the first place. See Premature Optimization for more details.
...
Code Block |
---|
from coordinate import Coordinate # Good: Name is derived from the type Coordinate coordinate = Coordinate() # Good: Name is suffixed with the type and overall has more context (represents coordinate of a vehicle) vehicle_coordinate = Coordinate() # Bad: Name deviates from the type Coordinate; this will confuse other readers location = Coordinate() # Good: Name denotes that the variable is a pair (tuple) and is explicit about the data structure latitude_longitude_pair = (coordinate.latitude, coordinate.longitude) # Debatable: Sometimes names may be too long or too detailed. It may already be implied that latitude and longitude are best represented as floats latitude_longitude_float_pair = (coordinate.latitude, coordinate.longitude) coordinate = Coordinate() # Good: Plural name clearly implies a list of coordinate objects coordinates = [ coordinate, ] # Somewhat good: Although the suffix "dict" makes it clear that this is a dictionary, readers also need context on the dictionary's schema coordinate_dict = { "current": coordinate } # Good: Suffix makes it clear that this is a set (unique set of coordinates) coordinate_set = { coordinate } # Good: Suffix suffix "dict" makes it clear that this is a string representing a coordinate coordinate_str = "0.0, 0.0" |
Sometimes it is fine to assume the name is implicitly clear if there is enough context surrounding the names (this is mostly applicable for well-known conventions or is typically the case when the readers have domain knowledge).
Code Block |
---|
from memoryblock import MemoryBlock
# Memory block represents a simple block of memory starting at some address of some size
memory_block = MemoryBlock()
memory_block.name # Assume this attribute is a string representing the name of the memory block
memory_block.address # Assume this attribute is an integer representing the start address
memory_block.size # Assume this attribute is an integer representing the size in bytes |
Filesystem
Python developers will often deal with the filesystem in some form; some applications that rely on the filesystem include general automation, file/directory manipulation, and command line tool.
Fundamental concepts
It is important to understand the fundamentals of filesystem such as:
Here are some guidelines:
Programs should assume the current directory can be anywhere (i.e. do not assume current directory will be in the same directory as the entry point file)
Programs should generally work regardless if given absolute paths or a relative paths (it depends on how the paths are provided)
Programs should be OS agnostic and work regardless if dealing with POSIX paths or Windows paths
Other
Apart from precondition checks at the beginning of a function/method, each function/method should only have one return statement and that should be at the end of the function. This is to prevent unintentional side-effects where a developer adds some code thinking it will be run, but the function exits out before it gets to that piece of code.
Naming convention
Similar to the general naming convention in the earlier section, filesystem paths should have its own naming convention. The name of the variable should represent a filesystem node file vs. directory
and should be explicit about the representation name vs. path vs. name with no extension
.
Here are some naming guidelines:
Variable representing a directory path should be suffixed with
dirpath
ordirname
Variable representing a file path should be suffixed with
filepath
orfilename
Variable representing any path should be suffixed with
nodepath
ornodename
Example
Code Block |
---|
from datetime import datetime
import os
from pathlib import Path
# Good: It is clear that this variable represents a directory containing N log files
log_dirpath = Path("workspace/logs")
# Bad: "logs" can easily be misinterpreted as a list of log objects
logs = Path("workspace/logs")
timestamp_str = datetime.now().strftime("%m-%d-%Y_%H-%M-%S")
# Good: It is clear that this variable represents a file path relative to the current directory
log_filepath = Path("log_{}.txt".format(timestamp_str))
# Good: It is clear that this variable represents a file name and does not represent an addressable path by itself
log_filename = "log_{}.txt".format(timestamp_str)
# Good: The suffix nodepath implicitly denotes that this function takes file or directory paths
def copy(src_nodepath, dest_nodepath):
"""
Copy a source path to the destination path
If the given paths are directory paths, perform a recursive copy
"""
raise NotImplementedError
# Bad: The name "file" can easily be misinterpreted as a file object.
# The function "os.listdir" actually returns a list of node names (names of both directories and files)
for file in os.listdir(log_dirpath):
print(file) |
Current Directory
From a program’s perspective, the current directory can be anywhere in the filesystem; it is up to the caller of the program.
Code Block | ||
---|---|---|
| ||
./program.py # Current directory is in the same directory as the program file
./directory/program.py # Current directory is at the parect directory of the program file
/home/root/directory/program.py # Current directory could be anywhere in the filesystem |
It is good practice to ensure your program is agnostic to the location of the current directory. Some good ways to make your program agnostic of the current directory is to:
Derive the parent directory of the program file, then assemble paths relative to the location of the program file itself
Let the caller determine the location of a directory/file by taking path(s) as commend line option(s). This avoids having your program make assumptions of the host’s filesystem layout
Code Block | ||
---|---|---|
| ||
"""
Example, derive the parent directory of this file, then assemble paths relative to the parent directory
This approach is great if this program is packaged with other files that live alongside it (e.g. configuration files)
"""
import os
from pathlib import Path
# Ways to get the parent directory path of this program file
self_dirpath = os.path.dirname(__file__) # Using traditional os to get a string object
self_dirpath = Path(__file__).parent # Using modern pathlib to get a Path object
# Ways to assemble a path relative to the parent directory of this file
target_filepath = os.path.join(self_dirpath, "directory", "test.txt") # Assume self_dirpath is a string object
target_filepath = self_dirpath.joinpath("directory", "test.txt") # Assume self_dirpath is a Path object |
Code Block | ||
---|---|---|
| ||
"""
Example, let the caller of this program provide the path to use
This example uses pathlib Path objects instead of os string objects
"""
from argparse import ArgumentParser
from pathlib import Path
import sys
def get_args():
argument_parser = ArgumentParser()
argument_parser.add_argument("-d", "--target-dirpath", type=Path, help="A directory path to reference")
return argument_parser.parse_args()
def main():
args = get_args()
target_filepath = args.target_dirpath.joinpath("directory", "test.txt")
if __name__ == "__main__":
sys.exit(main()) |
Changing the Current Directory
Your program (whether it’s Python or a shell script) should avoid changing the caller’s current directory if possible.
Code Block | ||
---|---|---|
| ||
/home/user$ ./some-blackbox-program # <=== Program changes the current directory during execution
/tmp$ # <=== Program exited; current directory changed (Avoid this behavior if possible) |
If the current directory needs to be changed for any reason, ensure it gets reverted by the end of the program.
Code Block | ||
---|---|---|
| ||
import os prev_current_directory = os.getcwd() try: os.chdir("/tmp") # Do stuff here finally: os.chdir(prev_current_directory)dictionary, readers also need context on the dictionary's schema coordinate_dict = { "current": coordinate } # Good: Suffix makes it clear that this is a set (unique set of coordinates) coordinate_set = { coordinate } # Good: Suffix makes it clear that this is a string representing a coordinate coordinate_str = "0.0, 0.0" |
Sometimes it is fine to assume the name is implicitly clear if there is enough context surrounding the names (this is mostly applicable for well-known conventions or is typically the case when the readers have domain knowledge).
Code Block |
---|
from memoryblock import MemoryBlock
# Memory block represents a simple block of memory starting at some address of some size
memory_block = MemoryBlock()
memory_block.name # Assume this attribute is a string representing the name of the memory block
memory_block.address # Assume this attribute is an integer representing the start address
memory_block.size # Assume this attribute is an integer representing the size in bytes |
Filesystem
Python developers will often deal with the filesystem in some form; some applications that rely on the filesystem include general automation, file/directory manipulation, and command line tool.
Fundamental concepts
It is important to understand the fundamentals of filesystem such as:
Here are some guidelines:
Programs should assume the current directory can be anywhere (i.e. do not assume current directory will be in the same directory as the entry point file)
Programs should generally work regardless if given absolute paths or a relative paths (it depends on how the paths are provided)
Programs should be OS agnostic and work regardless if dealing with POSIX paths or Windows paths
Current Directory
From a program’s perspective, the current directory can be anywhere in the filesystem; it is up to the caller of the program.
Code Block | ||
---|---|---|
| ||
./program.py # Current directory is in the same directory as the program file
./directory/program.py # Current directory is at the parect directory of the program file
/home/root/directory/program.py # Current directory could be anywhere in the filesystem |
It is good practice to ensure your program is agnostic to the location of the current directory. Some good ways to make your program agnostic of the current directory is to:
Derive the parent directory of the program file, then assemble paths relative to the location of the program file itself
Let the caller determine the location of a directory/file by taking path(s) as commend line option(s). This avoids having your program make assumptions of the host’s filesystem layout
Code Block | ||
---|---|---|
| ||
"""
Example, derive the parent directory of this file, then assemble paths relative to the parent directory
This approach is great if this program is packaged with other files that live alongside it (e.g. configuration files)
"""
import os
from pathlib import Path
# Ways to get the parent directory path of this program file
self_dirpath = os.path.dirname(__file__) # Using traditional os to get a string object
self_dirpath = Path(__file__).parent # Using modern pathlib to get a Path object
# Ways to assemble a path relative to the parent directory of this file
target_filepath = os.path.join(self_dirpath, "directory", "test.txt") # Assume self_dirpath is a string object
target_filepath = self_dirpath.joinpath("directory", "test.txt") # Assume self_dirpath is a Path object |
Code Block | ||
---|---|---|
| ||
"""
Example, let the caller of this program provide the path to use
This example uses pathlib Path objects instead of os string objects
"""
from argparse import ArgumentParser
from pathlib import Path
import sys
def get_args():
argument_parser = ArgumentParser()
argument_parser.add_argument("-d", "--target-dirpath", type=Path, help="A directory path to reference")
return argument_parser.parse_args()
def main():
args = get_args()
target_filepath = args.target_dirpath.joinpath("directory", "test.txt")
if __name__ == "__main__":
sys.exit(main()) |
Changing the Current Directory
Your program (whether it’s Python or a shell script) should avoid changing the caller’s current directory if possible.
Code Block | ||
---|---|---|
| ||
/home/user$ ./some-blackbox-program # <=== Program changes the current directory during execution
/tmp$ # <=== Program exited; current directory changed (Avoid this behavior if possible) |
If the current directory needs to be changed for any reason, ensure it gets reverted by the end of the program.
Code Block | ||
---|---|---|
| ||
import os
prev_current_directory = os.getcwd()
try:
os.chdir("/tmp")
# Do stuff here
finally:
os.chdir(prev_current_directory) |
Naming convention
Similar to the general naming convention in the earlier section, filesystem paths should have its own naming convention. The name of the variable should represent a filesystem node file vs. directory
and should be explicit about the representation name vs. path vs. name with no extension
.
Here are some naming guidelines:
Variable representing a directory path should be suffixed with
dirpath
ordirname
Variable representing a file path should be suffixed with
filepath
orfilename
Variable representing any path should be suffixed with
nodepath
ornodename
Example
Code Block |
---|
from datetime import datetime
import os
from pathlib import Path
# Good: It is clear that this variable represents a directory containing N log files
log_dirpath = Path("workspace/logs")
# Bad: "logs" can easily be misinterpreted as a list of log objects
logs = Path("workspace/logs")
timestamp_str = datetime.now().strftime("%m-%d-%Y_%H-%M-%S")
# Good: It is clear that this variable represents a file path relative to the current directory
log_filepath = Path("log_{}.txt".format(timestamp_str))
# Good: It is clear that this variable represents a file name and does not represent an addressable path by itself
log_filename = "log_{}.txt".format(timestamp_str)
# Good: The suffix nodepath implicitly denotes that this function takes file or directory paths
def copy(src_nodepath, dest_nodepath):
"""
Copy a source path to the destination path
If the given paths are directory paths, perform a recursive copy
"""
raise NotImplementedError
# Bad: The name "file" can easily be misinterpreted as a file object.
# The function "os.listdir" actually returns a list of node names (names of both directories and files)
for file in os.listdir(log_dirpath):
print(file) |
Other
Apart from precondition checks at the beginning of a function/method, each function/method should only have one return statement and that should be at the end of the function. This is to prevent unintentional side-effects where a developer adds some code thinking it will be run, but the function exits out before it gets to that piece of code.
...
File layout
Defining a convention for file layouts is important for consistency. All Python files should have a defined purpose, and Python files should not be created simply for the sake of dividing a program into different modules.
...