Best Ways to Write Clean and Quality Python Code
The article below will provide insights that can be helpful in your day-to-day coding and improve the quality of your Python code.
Join the DZone community and get the full member experience.
Join For FreeWhen considering whether to use classes in Python code, it's important to weigh the benefits and contexts where they are most appropriate. Classes are a key feature of object-oriented programming (OOP) and can provide clear advantages in terms of organization, reusability, and encapsulation. However, not every problem requires OOP.
When to Use Classes
Encapsulation and Abstraction
- Encapsulation. Classes allow you to bundle data (attributes) and methods (functions) that operate on the data into a single unit. This helps in keeping related data and behaviors together.
- Abstraction. Classes can hide the complex implementation details and expose only the necessary parts through public methods.
Reusability
- Reusable components. Classes can be represented multiple times, allowing you to create reusable components that can be used in different parts of your application.
Inheritance and Polymorphism
- Inheritance. Classes allow you to create new classes based on existing ones, reusing the code and reducing redundancy.
- Polymorphism. Classes enable you to define methods in such a way that they can be used interchangeably, improving flexibility and integration.
Stateful Function Using Classes
Using a class is a more explicit way to manage state. Use classes when the stateful function requires multiple methods.
class Counter
def__init__(self):
self.count =0
def increment(self):
self.count =1
return self.count
# Create a counter instance
counter1 = Counter()
print(counter1.increment()) #Output:1
print(counter1.increment()) #Output:2
print(counter1.increment()) #Output:3
# Create another counter instance
counter2 = Counter()
print(counter2.increment()) #Output:1
print(counter2.increment()) #Output:2
When Not to Use Classes
Simple Scripts and Small Programs
For small scripts and simple programs, using functions without classes can keep the code straightforward and easy to understand.
Stateless Functions
If your functions do not need to maintain any state and only perform operations based on input arguments, using plain functions is often more appropriate.
Best Practices for Using Classes
Naming Conventions
- Use
CamelCase
for class names. - Use descriptive names that convey the purpose of the class.
Single Responsibility Principle
Ensure each class has a single responsibility or purpose. This makes them easier to understand and maintain.
Best Practices for Writing Functions
Function Length and Complexity
- The functions should be small, with the focus on a single item.
- Aim for functions to be no longer than 50 lines; if they are longer, consider refactoring.
Pure Functions
Write pure functions where possible. Its output is determined only by its input and has no side effects. It always produces the same output for the same input. It has several benefits, including easier testing and debugging, as well as better code maintainability and reusability.
They also facilitate parallel and concurrent programming since they don't rely on the shared state or mutable data.
def add(a,b):
"""Add two numbers."""
return a + b
#Usage
result = add(4,2)
print(result) # Output:6
Impure Functions
It relies on external state and produces side effects, which makes its behavior unpredictable.
total = 0
def add_to_total(value):
"""Add a value to the global total"""
global total
total += value
#Usage:
add_to_total(4)
print(total) #Output: 4
add_to_total(2)
print(total) #Output: 6
Documentation
- Use docstrings to document the purpose and behavior of functions
- Include type hints to specify the expected input and output types
Static Methods
It is used when you want a method to be associated with a class rather than an instance of the class. Below are a few scenarios.
- Grouping utility functions. It is a group of related functions that logically belong to a class but don't depend on instance attributes or methods.
- Code organization. It helps in organizing the code by grouping related functions with in a class, even if they don't operate on instance-level data.
- Encapsulation. It clearly communicates that they are related to the class, not to any specific instance, which can enhance code readability.
DateUtils.py
from datetime import datetime
class Dateutils:
@staticmethod
def is_valid_date(date_str, format="%Y-%m-%d"):
"""
checks if a string is a valid date in the specified format.
Args:
date_str(Str): the date strig to validate.
format(str, optional): the date format to use (default "%Y-%m-%d").
Returns:
bool: true if the date is valid, false otherwise.
"""
try:
datetime.strptime(date_str, format)
return True
except ValueError:
return False
@staticmethod
def get_days_difference(date1_str, date2_str, format ="%Y-%m-%d"):
"""
calculates number of days between two dates
Args:
date1_str(Str): the first date string.
date2_str(Str): the second date string.
format(str, optional): the date format to use (default "%Y-%m-%d").
Returns:
int: the number of days between two dates.
"""
date1 = datetime.strptime(date1_str, format)
date2 = datetime.strptime(date2_str, format)
return abs(date2 - date1).days) # abs() is used for non negative difference
#usage
valid_date = DateUtils.is_valid_date("2025-03-07")
print(f"Is'2025-03-07' is a valid date? {valid_date}") #output:True
days_diff = DateUtils.get_days_difference("2025-02-01","2025-03-01")
print(f"Days between '2025-02-26' and '2025-03-01': {days_diff}"
Logger
It's a built-in logging module that is a flexible and powerful system for managing application logs. It is essential for diagnosing issues and monitoring application behavior.
The RotatingFileHandler
is a powerful tool for managing log files. It allows you to create log files that automatically rotate when they reach a certain size.
log_base_config.py
import logging
from logging.handlers import RotatingFileHandler
def setup_logging():
#create logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)
#create a rotating file handler
rotating_handler = RotatingFileHandler('example.log', maxBytes=1024, backupcount =30)
rotating_handler.setLevel(logging.INFO)
#create a formatter and add it to the handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
rotating_handler.setFormatter(formatter)
#add rotating file handler to the logger
logger.addHandler(rotating_handler)
logger_impl.py
import logging
from log_base_config import setup_loggimg
setup_logging()
logger = logging.getLogger("logger_example")
def call_logger(numerator:int,denominator:int):
logger.info("call_logger(): start")
#log some messages
for i in range(32):
logger.info(f'This is a message {i}')
try:
result = numerator / denominator
logger.info("Division successful: %d / %d = %f", numerator, denominator, result)
return result
except ZeroDivisionError:
logger.error("Failed to perform division due to a zero division error.")
return None
logger.info("call_logger(): end")
if __name__ == "__main__":
call_logger(12,0)
Do Not Consume Exception
It is catching an exception without taking appropriate action such as logging the error or raising the exception again. This can make debugging difficult because the program fails silently. Instead you should handle exception in a way that provides useful information and allows the application to either recover or fail gracefully.
Here's how you can properly handle exceptions without consuming them:
- Ensure the exception details are logged.
- Provide user-friendly feedback or take corrective actions.
- If appropriate, raise the exception again after logging it.
Below is an example showing how to properly handle and log a ZeroDivisionError
without consuming the exception.
import logging
from log_base_config import setup_loggimg
setup_logging()
logger = logging.getLogger("logger_example")
def divide_numbers(numerator, denominator):
try:
result = numerator / denominator
logger.info("Division successful: %d / %d = %f", numerator, denominator, result)
return result
except ZeroDivisionError as e:
logger.error("Division by zero error: %d / %d", numerator, denominator)
logger.exception("Exception details:")
#raise the exception again after logging
raise
# test the function
num = 10
denon = 0
try
logger.info("Attempting to divide %d by %d", num, denom)
result = divide_number(num, denom)
except ZeroDivisionError:
logger.error("Failed to perform division due to a zero division error.")
Conclusion
By following these techniques, one can ensure that exceptions are handled transparently, aiding in debugging and maintaining robust application behavior.
Opinions expressed by DZone contributors are their own.
Comments