Write Code Pythonic Way

Write Code Pythonic Way

It is a good practice that you write more Pythonic code in your program. It not only makes the code more readable but also makes it easier to maintain. In this article I will guide you to write readable and clean Python code. We will follow some of the most used best practices to write python code. We will focus on the following -

  1. Naming conventions

  2. Functions

  3. Docstring

Variable and Function Name

Variable and function names should have only lowercase letters. If there are more than one word, these should be separated by _, underscore.

name = "Ritwik Math" # Single word
first_name = "Ritwik" # Multiple words
last_name = "Math" # Multiple words

def greet(): # Single word
    ...

def greet_all(): # Multiple words
    ...

For methods follow the same naming convention.

class Payment:
    def get_payment_details_by(customer_id: str) -> list:
        ...

Class Name

We should write Class names in CamalCase.

class Payment: # Single word
    ...

class PaymentWebhook: # Multiple words
    ...

Protected, Private and Name Dangling

Use single underscore and double underscore to make a variable protected and private respectively.

class Payment:
    def __init__(self):
        _gateway = 'stripe' # Protected
        __type = 'online' # Private

To prevent name dangling, use double underscore.

class Payment:
    def __init__(self):
        _gateway = 'stripe'
        __type = 'online' # type is a keyword, __type is not
        __class = 'Payment' # class is a keyword, __class is not

Easy to Understand Function Names

class Payment:
    def get_payment_details_by(customer_id: str) -> list:
        ...

In this example, it is easy to read where it is defined. But if you call this method, the name does not describe it properly. Method get_payment_details_by does not give you any information on which data it uses to filter the payment details. A better name for this method is get_payment_details_by_customer_id.

class Payment:
    def get_payment_details_by_customer_id(id: str) -> list:
        ...

A function, variable or method should be able to describe its purpose in brief.

Constant

Constant name should be in uppercase. If name consists of multiple words, those words must be separated by underscore, _.

APP = "PAYMENT_GATEWAY"
ROOT_LOCATION = "/usr/src/app"

Use Named Function instead of Anonymous

Lambda in Higher Order Functions

Using lambda can make help you write one-liners. However, it reduces the code readability. Whenever possible you should define a named function. In higher order function you may write a lambda function to filter or alter list elements.

students = [
    {"name": "Ritwik Math", "dept": "CSE", "age": "30"},
    {"name": "john doe", "dept": "CHEMICAL", "age": "29"},
    {"name": "jane doe", "dept": "CHEMICAL", "age": "30"}
]

sorted_student_list = sorted(students, key=lambda s: (int(s["age"]), s["name"].lower()))

In order to understand how the list is sorted, you have to read the entire code. It can be hard when you are finding a bug for hours. Lambda being a anonymous function, does not give a hint, based on what keys the sort need to be done. If you write a named function, it will save you time in future every-time you are debugging your code.

students = [
    {"name": "Ritwik Math", "dept": "CSE", "age": "30"},
    {"name": "john doe", "dept": "CHEMICAL", "age": "29"},
    {"name": "jane doe", "dept": "CHEMICAL", "age": "30"}
]

def sort_using_name_age(student: dict) -> (str, int):
    if not student["age"].isdigit():
        raise ValueError("age is not an integer")
    return student["name"].lower(), int(student["age"])

sorted_student_list = sorted(students, key=sort_using_name_age)

Suppose, actual sort order was first name then age. But in the first example, there was no hint about it. There was no scope for you to test the data type. You can now do anything in the if block. You can set a default value, or log this error, raise customer error and anything that suits your application requirement. Now, just by seeing the function name you can guess the purpose of entire sorting process.

Function Declaration

In python, functions are first-class citizen. So, function can be assigned to a variable. Assigning lambda to a variable can save you a lot of time by allowing one-liner.

greet = lambda name: f"Hello {name}"

Requirements changed, based on local time you have to generate the greet message. You have to document this info into docstring as well. In this scenario you cannot use lambda any more.

Lambda is also ephemeral. If we are trying to assign a name to anonymous then the purpose of it completely lost. Instead we can declare a named function that is capable of handling everything that lambda can. Code becomes more consistent and easy to test.

def greet(name: str) -> str:
    """This function greets a person based on hour of the day"""
    if datetime.now().hour < 12:
        __time = "morning"
    elif datetime.now().hour == 12:
        __time = "noon"
    else:
        __time = "nigt"
    return f"Good {__time} {name}"

Consistency

Handling all Execution Paths

If a dict has age, your function converts it into integer and returns. This is a very simple function.

def convert_age_to_int(student: dict) -> int | None:
    if "age" in student and student["age"].isdigit():
        return int(student["age"])

Function automatically returns None when the condition is not met. But it is best practice to handle all the paths manually. We should write return None at the end so that everyone can understand it.

def convert_age_to_int(student: dict) -> int | None:
    if "age" in student and student["age"].isdigit():
        return int(student["age"])
    return None

Functionality wise it does not affect the behavior but it is good practice to handle all the execution path ourselves.

Maintaining Format

If you are using double quotes, " to represent string, make sure that is maintained in the entire code base. If you want to use single quotes, then use it everywhere.

def convert_age_to_int(student: dict) -> int | None:
    if "age" in student and student["age"].isdigit():
        return int(student['age']) # Single quotes used, not good
    return None

If application has a global exception handling function use it everywhere. Even if you try to catch the error make sure you raise it again. Example, for one specific function you are required to trigger an event.

def convert_age_to_int(age: str) -> int:
    try:
        return int(age)
    except ValueError as exc:
        pass # trigger an sns
        raise exc

try:
    convert_age_to_int("3a")
except Exception as exc:
    print(exc)

Using Docstring

Docstring is a convenient way of adding documentation to python modules.

 def convert_age_to_int(age: str) -> int:
    """Converts a string to integer value"""
    try:
        return int(age)
    except ValueError as exc:
        pass # trigger an sns
        raise exc

Function convert_age_to_int has a one-line docstring. However, it is a good practice to add multi-line docstrings to your modules.

 def convert_age_to_int(age: str) -> int:
    """Converts a string to integer value.

    Args:
      age: age value in string.

    Returns:
      Age value in integer.

    Raises:
      ValueError: If string is not a digit.
    """
    try:
        return int(age)
    except ValueError as exc:
        pass # trigger an sns
        raise exc

Multi-line docstring can provide a more detailed idea of the function. In the above example, not only we can see the description of the function but also what argument it expects, what it should return and any specific exceptions that can be raised within the function block.

Always use different docstring formats for different elements. You get better idea from this google site.