Code Standards for Python Developers
As the Clean Code book said, “Code is written once, but read many times”.
In my experience, it’s rare to write codes, and then it will be there, eternal and untouchable. Tomorrow, or maybe two months later I should change something due to new requirements or bug fixes. But when I returned to the code, I couldn’t even believe I had created this monster.
Readability is important because you (or your teammates) need to maintain your code someday.
So write your codes carefully, the future of you may need it.
In this article, I will tell you some Python code standards that would improve your code quality and make it more readable, scalable, and maintainable.
The reference for this code standards.
I created this standard based on PEP 8, Google Python Style Guide, and Clean Code: A Handbook of Agile Software Craftmanship book.
PEP 8 is the official Python style guide, written by Guido von Rossum (Python creator).
While Google Python Style Sheet is good stuff from Google, it covers so many things that PEP 8 didn't.
And clean code has been one of the holy books of programming.
I would suggest you read the three of them after finishing my article.
CODE STANDARDS FOR PYTHON DEVELOPER
Code formatter.
Code formatter makes your code beautiful, it handles some basic code standards like indentation, line height, white space, code max-width, etc. Black is my recommendation. It follows PEP 8 conventions and it was suggested by Google Python Style Guide.
Linter
You can use Pylint.
Linter is strict, so be careful to use it. When you work on legacy codes, implementing linter may cause errors everywhere. If it brings more problems than benefits itself, don’t use it.
Indentation
Use four spaces or a single tab to create an indentation. Do not mix tabs and spaces. Use one consistently.
numbers = [1, 2, 3]
total = 0
for num in numbers:
# here I use tab
if num != 0:
total += num
Maximum number of lines in a file.
The maximum number of lines in a file is about 500 lines of code. This limitation is used to prevent the accumulation of logical complexity in one file, so the program will be more modular and easier to understand.
Maximum line width of the codes.
80 characters is the max length of a line of code, it makes sure that your code is readable on small screen devices. 80 characters is the length of the following sentence.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, iste itaq..
This also makes things easier for developers when they compare two different pieces of code on the same screen.
Naming convention.
Use snake_case for naming convention standard.
Each word in a variable or function is written in lowercase and separated by an underscore (_).
product_brand = "Indomie"
def get_total_product(product_brand):
...
Folder names, file names, function names, and variable names should written using snake case.
# file_name
product_categrory_service.py
# folder_name
product_category
# variabel_name
product_brand = "Indomie"
# function_name
def get_total_product(product_brand):
...
For some cases, you shouldn’t use a snake case.
Class names are not written using snake cases. Instead, use CapWord or CamelCase.
class ProductService():
...
Constants or configs are written using capital letters separated by underscore(_).
PI = 3.14
DEFAULT_USER= 'admin'
Meaningful names.
Use meaningful names for each piece of your code, let it talk, and tell what it does. Let it self documented.
Variable or function names should describe their purpose.
days_since_creation = 17
file_age_in_days = 16
Function names should be verbs indicating what it does.
def get_real_name(self):
...
def save(self):
...
Class names should be nouns indicating that it is an entity or object.
class Customer:
...
class AddressParser:
...
class UserInfo:
...
Function.
Write your function properly.
Functions should be small.
The length of a function should not exceed 40 lines of code. Ideally, a function should perform only one task.
import math
def calculate_circle_area(radius, pi):
return pi * radius**2
def calculate_circle_circumference(radius, pi):
return 2 * pi * radius
def calculate_circle_properties(radius, pi):
area = calculate_circle_area(radius, pi)
circumference = calculate_circle_circumference(radius, pi)
return area, circumference
Use annotation in function.
Annotation makes it easier for other developers to understand a function through its descriptions.
from typing import Union
def greeting(name: str) -> str:
return "Hello " + name
def greeting_with_default_value(name: str = "John") -> str:
return "Hello " + name
def to_number(value: Union[int, str]) -> int:
return int(value)
Class.
A class should be small.
An ideal class should only be responsible for one specific thing.
class Greeter:
def greet(self, name):
return f"Hello, {name}!"
For a complex class, rather than doing many things, break the class into several small classes and compose them.
class FoodGenerator:
def __init__(self):
self.pizza = PizzaGenerator()
self.donuts = DonutsGenerator()
class PizzaGenerator:
...
class DonutsGenerator:
...
Use an underscore (_) to mark private attributes within a class.
Private attributes or functions should not be called from outside the class.
class MyCalculator:
def __init__(self, numbers):
self._numbers = numbers
def _remove_negative_numbers(self):
...
Functions within a class should be written in order.
Start with public functions, and then followed by private functions. Additionally, functions should be arranged based on their call order, with caller functions at the top, followed by the functions they call. This makes it easier for other developers to follow the flow.
class TextSanitizer:
def sanitize(self, text):
cleaned_text = self._remove_white_space_and_special_character(text)
...
def _remove_white_space_and_special_character(self, text):
text_with_no_white_space = self._remove_white_space(text)
cleaned_text = self._remove_special_character(text_with_no_white_space)
return cleaned_text
def _remove_special_character(self, text):
...
def _remove_white_space(self, text):
...
Doc String.
Use doc string to provide documentation about the purpose and usage of classes and functions.
Always use triple quotes to create a doc string.
def calculate_numbers():
"""Returns the total value"""
Doc strings should provide clear information on how to call a function or class.
def greet(name: str, age: int) -> str:
"""
Generate a greeting message based on name and age.
Args:
name (str): The name of the person to greet.
age (int): The age of the person.
Returns:
str: A greeting message including the person's name and age.
"""
return f"Hello, {name}! You are {age} years old."
class Person:
"""
A class representing a person with a name and age.
Attributes:
name (str): The name of the person.
age (int): The age of the person.
"""
def __init__(self, name: str, age: int):
self.name = name
self.age = age
Class, function, and public attributes must have doc strings.
Class, function, and public attributes are mostly called by another part of codes. Other developers may be going to use it. Providing clear documentation is a must.
If necessary, include examples of how to call the function/class.
def double_number(num: int):
"""
Double the given number.
Args:
num (int): The number to be doubled.
num2 (int): The number to be doubled.
Returns:
int: The result of doubling the input number.
Example:
>>> result = double_number(5)
>>> print(result)
10
"""
return num * 2
Comment.
Comments are Bad. This is why.
Comments are often outdated.
As the program flow changes, the code will be updated, but comments are often forgotten. Consequently, comments become inaccurate and tend to mislead.
Comments are evidence of a developer’s failure to create readable and self-documented code.
If the code is clear enough and self-documented, we shouldn’t need to explain it again by adding comments. If it’s hard to understand it, the correct solution is to improve the code quality, not to clarify it with comments.
Commenting on unused codes will leave questions for other developers.
Is this code really that important so the previous coder didn’t remove it? What is this used for? Then the other developers won’t touch it, because they don’t want to take any risk.
Considering those 3 things, you shouldn’t use comments.
But there are also conditions where comments are really useful.
Here are conditions where comments are good and useful.
They are giving additional information.
Some codes are really hard to explain, regex for example. Comments can help us to clarify something when the codes have no way to express it.
import re
# format matched kk:mm:ss EEE, MMM dd, yyyy
time_matcher = re.compile(r"\d*:\d*:\d* \w*, \w* \d*, \d*")
Explaining the reason for some decision-making.
# This is our best attempt to get a race condition,
# by creating a large number of threads.
Giving warnings about the consequences when executing a piece of code or something.
# Warning of consequences
# Don't run unless you
# have some time to kill.
Nested loops and conditional statements.
Avoid it.
Loops and nested conditional statements should be limited to 2 or 3 levels. The deeper it is, the more difficult to understand the program flow.
If you can’t avoid nested loops or nested conditional statements, split them into several callable functions.
This is an example of an overload function with a lot of nested loops and conditions.
def process_orders(orders):
for order in orders:
total_price = 0
for item in order["items"]:
total_price += item["price"] * item["quantity"]
for discount in order["discounts"]:
if discount["type"] == "percentage":
if total_price > discount["threshold"]:
total_price -= total_price * (discount["value"] / 100)
elif discount["type"] == "fixed":
if total_price > discount["threshold"]:
total_price -= discount["value"]
else:
if total_price > discount["threshold"]:
total_price -= total_price * 0.1
if total_price > 150:
for item in order["items"]:
for attr in item.get("attributes", []):
if attr["type"] == "color" and attr["value"] == "red":
print("Free red gift applied!")
else:
if total_price < 50:
for item in order["items"]:
for attr in item.get("attributes", []):
if attr["type"] == "size" and attr["value"] == "large":
print("Additional fee for large items!")
print(f"Order total: ${total_price:.2f}")
orders = []
process_orders(orders)
Split the process and make it more readable.
def calculate_total_price(order):
total_price = sum(item["price"] * item["quantity"] for item in order["items"])
return total_price
def apply_discounts(total_price, discounts):
for discount in discounts:
threshold = discount["threshold"]
if total_price > threshold:
if discount["type"] == "percentage":
total_price -= total_price * (discount["value"] / 100)
elif discount["type"] == "fixed":
total_price -= discount["value"]
else:
total_price -= total_price * 0.1
return total_price
def apply_free_gift(order):
total_price = calculate_total_price(order)
if total_price > 150:
has_red_color = any(
attr["type"] == "color" and attr["value"] == "red"
for item in order["items"]
for attr in item.get("attributes", [])
)
if has_red_color:
print("Free red gift applied!")
def apply_additional_fee(order):
total_price = calculate_total_price(order)
if total_price < 50:
has_large_size = any(
attr["type"] == "size" and attr["value"] == "large"
for item in order["items"]
for attr in item.get("attributes", [])
)
if has_large_size:
print("Additional fee for large items!")
def process_order(order):
total_price = calculate_total_price(order)
total_price = apply_discounts(total_price, order["discounts"])
apply_free_gift(order)
apply_additional_fee(order)
print(f"Order total: ${total_price:.2f}")
orders = []
for order in orders:
process_order(order)
Import.
Always place import statements at the top of a file.
import os
import sys
...
Order imports sequentially: start with the Python standard library, followed by third-party libraries, and then local libraries.
Separate these types of imports with blank lines.
import os
import sys
from flask import Flask
from my_database import connection
Whitespace.
Add blank lines above and below classes or functions to separate them from other classes or function declarations.
class Calculator:
def add(self, num1, num2):
pass
def divide(self, num1, num2):
pass
def sum(self, numbers):
pass
def calculate_total(numbers):
pass
Is Empty?
Use the default boolean value to check if the value is empty.
In Python, zero, none, empty containers (e.g., the [],{},(), set(), “ ”), and false are considered as False.
While non-zero numbers, non-empty containers, and true are considered as True.
Take advantage of this behavior. For example, to check whether a variable is empty or not.
records = []
if records:
...
In the case of “None”, it’s better to use the is operators. It sounds more human.
foo = None
if foo is None:
...
if foo is not None:
...
Exception.
Use exceptions to manage errors.
Exceptions class name should end with the word “Error” to indicate an exception class.
class InternalServerError(Exception):
...
class DatabaseConnectionError(Exception):
...
class CodeExecutionError(Exception):
...
Use exceptions to manage errors in the nested process.
In nested function calls, we tend to return operation status. As a result, the caller should evaluate the operation status and perform additional logic.
Look at the following example:
def read_file(filename):
try:
with open(filename, "r") as file:
return True, file.read(), None
except FileNotFoundError:
return False, None, "File not found"
def process_data(data):
if not data:
return False, None, "No data to process"
processed_data = data.upper() # Example processing
return True, processed_data, None
def save_data(processed_data, output_filename):
try:
with open(output_filename, "w") as file:
file.write(processed_data)
return True, None
except IOError:
return False, "Error saving data"
def process_file_data():
filename = "input.txt"
output_filename = "output.txt"
success, data, error = read_file(filename)
if not success:
print("Error:", error)
else:
success, processed_data, error = process_data(data)
if not success:
print("Error:", error)
else:
success, error = save_data(processed_data, output_filename)
if not success:
print("Error:", error)
The code became more complex and it’s hard to figure out what it does.
Using exceptions the right way could help you solve this problem.
Instead of returning the operation status, raise an exception when the expected condition is not fulfilled.
Then, let the final function handle the error based on the exceptions.
def read_file(filename):
try:
with open(filename, "r") as file:
return file.read()
except FileNotFoundError as e:
raise FileNotFoundError("Error reading file:", e)
def process_data(data):
if not data:
raise ValueError("No data to process")
return data.upper() # Example processing
def save_data(processed_data, output_filename):
try:
with open(output_filename, "w") as file:
file.write(processed_data)
except IOError as e:
raise IOError("Error saving data:", e)
def process_file_data():
filename = "input.txt"
output_filename = "output.txt"
try:
data = read_file(filename)
processed_data = process_data(data)
save_data(processed_data, output_filename)
except (FileNotFoundError, ValueError, IOError) as e:
print("Error:", e)
Magic Number.
Avoid using magic numbers. Magic numbers are numbers used in calculations without clear meaning.
For example:
radius = 12
circle_area = 3.14 * (radius**2)
What exactly is 3.14?
Instead of doing it that way, store the number in a variable to provide a clearer meaning.
radius = 12
pi = 3.14
circle_area = pi * (radius**2)
Arguments.
Pass object as an argument for functions with a lot of parameters.
Sometimes functions need to retrieve a lot of arguments, so it creates a lot of parameters.
from datetime import date
def add_person(
name: str,
age: int,
gender: str,
address: str,
email: str,
phone_number: str,
job: str,
company: str,
birth_date: date,
nationality: str,
):
...
Too many parameters make it ugly. As an alternative, we could use class or dataclass to create an object that can store the arguments.
Pass the object as an argument.
from dataclasses import dataclass
from datetime import date
@dataclass
class Person:
name: str
age: int
gender: str
address: str
email: str
phone_number: str
job: str
company: str
birth_date: date
nationality: str
def add_person(person: Person):
...
args = {"name": "John doe",} # and so on
person = Person(**args)
add_person(person)
Replace Empty Value.
Use the ‘or’ operator to replace an empty value.
Oftentimes, we use this way to replace empty values.
data = {}
id = data.get("id") if data.get("id") else 1
# or this way
id = data.get("id")
if not id:
id = 1
Using the ‘or’ operator makes it simpler and clearer.
id = data.get("id") or 1
Math Calculation.
Separate complex calculations with new lines.
To make calculations more readable, add a new line before each operation and put the operator in the front.
income = (
gross_wages
+ taxable_interest
+ (dividends - qualified_dividends)
- ira_deduction
- student_loan_interest
)
Nothing is perfect.
Standards are used to help us create better codes. If you face conditions where you may have to abandon the standard for performance or business process clarity. Go for it. Nothing is perfect.
Thanks for reading my articles.
Leave a clap 👏 or comment if you like it.