How are devs coding with AI?

Help us find out-take the survey.

8 Python Code Refactoring Techniques: Tools & Practices

Code refactoring is the process of restructuring existing code without changing its external functionality. Organizing the logic improves code readability and makes the code easier to debug and test. Developers use refactoring to eliminate redundancy and simplify complex structures to create cleaner and more efficient programs.

Consider an example of a poorly written Python code below. This is very hard to maintain due to several reasons – poor naming conventions, using magic numbers (hardcoded numeric values), no comments/explanations, and more. It not only is difficult to identify the purpose of this code, but also has the potential to pile up massive technical debt in the future. Continue reading to understand various refactoring techniques, which will help you write high quality code.

def calc(l):
    r=0
    for i in l:
        if i>10:
            r+=i*2
        else:
            r+=i
    return r/len(l) if len(l)>0 else 0
# Usage
nums=[5,12,3,15,8]
print(calc(nums))  # No error handling, unclear what the function does

In this guide, we will explore how to refactor Python code to improve readability. We will break down the common challenges and introduce structured approaches to make the Python code efficient. We will cover:

  • Why refactoring is important for Python development
  • Challenges faced while refactoring Python projects
  • Techniques to refactor code with practical examples
  • Useful Python refactoring tools for automation
  • Best practices for writing clean and efficient Python code

By the end, you will have hands-on knowledge of how to properly restructure and organize Python code using proper refactoring techniques and tools.

Why do we need to refactor Python code?

As Python projects grow over time, unoptimized Python code with extensive functions and duplicate logic can lead to performance inefficiencies. These make the codebase harder to modify, test, or extend. Not only do these create unnecessary complexities, but they also increase the technical burden on developers. Without proper refactoring, debugging becomes difficult, and maintaining the codebase takes more effort. Addressing these challenges through systematic refactoring keeps the codebase flexible and improves overall software quality.

Performing consistent Python code refactoring can introduce the following benefits:

  • Improved code readability: Code becomes more structured and easier to understand.
  • Enhanced maintainability: Future modifications and updates are easier to implement.
  • Better performance: Optimization techniques improve execution speed and efficiency.
  • Easier debugging & testing: Cleaner code results in fewer errors and easier debugging.

However, refactoring Python Code bears significant challenges as it requires careful planning to avoid breaking existing functionality. Developers often struggle to identify code segments that need improvement without introducing errors. Large codebases with tight dependencies make modifying code without unintended side effects difficult. Legacy code with unclear logic can slow down the process. Such challenges make Python code refactoring problematic for maintaining consistent code quality. These are the main reasons why we are required to implement the proper refactoring techniques.

Python code refactoring techniques

Refactoring involves various techniques to enhance code quality. Different refactoring techniques address issues like reducing redundancy, simplifying logic, and optimizing performance. By applying structured refactoring methods, we ensure the code remains clean and scalable as projects grow. Below, we discuss some widely used Python refactoring techniques with practical examples.

#1 Avoid hard coding

Developers often hard-code values like API keys, file paths, or configuration settings in Python scripts. Hard coding refers to embedding fixed values directly into the Python code, making the code difficult to modify or scale. Updating such scripts requires changing multiple parts of the codebase and increases the risk of errors.

Instead, we should replace hard-coded values with named constants, environment variables, or configuration files. Avoiding hard coding improves code flexibility and allows us to modify values easily without altering the program’s logic.

Example of hard-coded values

def calculate_discount(price):
    discount_rate = 0.10  # Hard-coded discount value
    discounted_price = price - (price * discount_rate)
    return discounted_price

print(calculate_discount(100))

In this example, the discount rate is hard-coded as 0.10, which is difficult to modify without directly changing the function. If we need to apply different discount rates in various scenarios, we would have to edit the code multiple times. Instead, we can define a named constant or use an external configuration file to store this value. This makes the code more adaptable and easier to manage.

Updated code after refactoring

DISCOUNT_RATE = 0.10  # Defined as a named constant

def calculate_discount(price):
    discounted_price = price - (price * DISCOUNT_RATE)
    return discounted_price

print(calculate_discount(100))

By defining DISCOUNT_RATE as a constant, we ensure that any changes only need to be made in one place. This reduces the risk of errors and improves maintainability, especially in larger applications. If needed, we can further improve flexibility by storing the value in an external configuration file or using environment variables. Qodo Gen helps you to improve your code.

The Qodo Gen chat available in VSCode comes bundled with all the knowledge you need to write the best version of Python code you need. As seen from the screenshot below, simply selecting the file after the /improve command provides multiple suggestions for the above hard-coded value example.

#2 Remove duplication

Duplicate code increases maintenance effort and leads to inconsistencies. When the same logic is repeated in multiple places, updating the code requires modifying each occurrence separately. Removing duplication follows the DRY (Don’t Repeat Yourself) principle, which promotes reusing code through functions, loops, or object-oriented programming. By refactoring duplicate code into reusable components, we reduce redundancy and make future modifications easier without affecting multiple parts of the codebase.

Example of duplicate code

def calculate_area_rectangle(length, width):
    return length * width

def calculate_area_square(side):
    return side * side  # Duplicate logic for calculating area

In this example, the logic for calculating the area is duplicated across two functions. While a square is a special case of a rectangle where both sides are equal, we have written separate functions for each. Instead of duplicating logic, we can create a single function that handles both cases, improving reusability and making the code more efficient.

Refactored code with reusability

def calculate_area(length, width=None):
    if width is None:
        width = length  # Handles square case
    return length * width

print(calculate_area(5))      # Square
print(calculate_area(5, 10))  # Rectangle

Using a single function, we eliminate redundancy while keeping the logic simple. If width is not provided, the function assumes a square by setting width = length. This approach makes the code more concise and easier to update, ensuring consistent calculations across different cases.

#3 Split-Up large functions

Large functions make code harder to read and debug. Modifying such functions without the unintended side effects becomes problematic when it handles multiple responsibilities. Splitting it into multiple, smaller, and well-defined functions improves code readability, reusability, and also improves maintenance efforts. Ideally, each function should follow the Single Responsibility Principle (SRP) to perform only one task, making the code easier to manage and enhancing modularity to reuse functions independently.

Example of a large function

def process_order(order):
    # Validate order
    if not order.get("items"):
        return "Order must contain items"
    
    # Calculate total price
    total = sum(item["price"] * item["quantity"] for item in order["items"])
    
    # Apply discount
    if order.get("discount_code") == "SAVE10":
        total *= 0.9  

    # Generate receipt
    receipt = f"Total: ${total:.2f}"
    return receipt

This function performs multiple tasks: validating an order, calculating the total price, applying a discount, and generating a receipt. If any part of the logic changes, we must modify this entire function, increasing complexity. Instead, we should break it into smaller, specialized functions.

Refactored code with smaller functions

def validate_order(order):
    if not order.get("items"):
        return False, "Order must contain items"
    return True, ""

def calculate_total(order):
    return sum(item["price"] * item["quantity"] for item in order["items"])

def apply_discount(total, discount_code):
    return total * 0.9 if discount_code == "SAVE10" else total

def generate_receipt(total):
    return f"Total: ${total:.2f}"

def process_order(order):
    valid, message = validate_order(order)
    if not valid:
        return message
    
    total = calculate_total(order)
    total = apply_discount(total, order.get("discount_code"))
    
    return generate_receipt(total)

Now, each function has a clear responsibility, which makes the code more readable and easier to modify. If we need to change how discounts work, we only update apply_discount() instead of modifying the entire process_order(). This structured approach improves code maintainability and simplifies debugging.

#4 List comprehension

List comprehension is a technique that simplifies loops and improves readability in Python codebase. Instead of using traditional loops to generate or filter lists, list comprehension provides a more concise and efficient way to achieve the same result. This method reduces boilerplate code and makes Python code look cleaner. It is beneficial for transforming and generating new lists in a single line.

Example without list comprehension

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = []
for num in numbers:
    if num % 2 == 0:
        even_numbers.append(num)
print(even_numbers)  # Output: [2, 4, 6, 8, 10]

In the example above, a for loop iterates through a list, checks for even numbers and appends them to another list. While functional, this approach is verbose and can be simplified. We can instead use list comprehension to achieve the same result in a single line, improving efficiency and readability.

Refactored code using list comprehension

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [num for num in numbers if num % 2 == 0]
print(even_numbers)  # Output: [2, 4, 6, 8, 10]

Now, the code is shorter and easier to read. The logic remains the same but is more efficient and expressive. List comprehension eliminates the need for an explicit loop and conditional statements inside the loop. This technique helps transform lists and reduce unnecessary lines of code in Python scripts.

Qodo Gen goes above and beyond in providing better suggestions. When Qodo Gen was asked to /improve the list comprehension issue above, it came up with multiple suggestions including the solution we discussed. You can choose to accept or reject the suggestions from Qodo, as seen from the screenshot below.

#5 Simplify complex conditions

Complex conditional statements with multiple if conditions can make the code hard to read and manage. A condition involving multiple logical operators and nested checks reduces code clarity. Simplifying such conditions using Boolean variables, functions, or dictionary mappings makes the logic more understandable and structured.

Example without simplification

def check_access(user):
    if (user["role"] == "admin" or user["role"] == "manager") and user["is_active"] and not user["is_suspended"]:
        return "Access Granted"
    else:
        return "Access Denied"

user = {"role": "manager", "is_active": True, "is_suspended": False}
print(check_access(user))  # Output: Access Granted

The condition inside the if statement is long and difficult to interpret at first glance. It combines multiple checks within a single line, making debugging harder. To simplify, we can use meaningful Boolean variables to break the condition into readable components.

Refactored code with simplified conditions

def check_access(user):
    has_permission = user["role"] in ["admin", "manager"]
    is_active = user["is_active"]
    is_not_suspended = not user["is_suspended"]
    
    if has_permission and is_active and is_not_suspended:
        return "Access Granted"
    return "Access Denied"

user = {"role": "manager", "is_active": True, "is_suspended": False}
print(check_access(user))  # Output: Access Granted

In the refactored code, the logic is structured and easier to debug. Each condition is assigned a descriptive variable, making the check’s intent self-explanatory. Instead of deeply nested conditions, we use separate Boolean expressions to improve both readability and maintainability.

#6 Replace temp with query

Temporary variables store intermediate values, but overusing them can clutter code and make it harder to follow. Instead of assigning values to temporary variables, using functions and queries keeps the logic clean and improves code readability. This technique is useful when a temporary variable stores derived values from other attributes.

Example without refactoring

class Order:
    def __init__(self, quantity, price):
        self.quantity = quantity
        self.price = price

    def get_total_price(self):
        total = self.quantity * self.price  # Temporary variable (unnecessary)
        return total

order = Order(5, 20)
print(order.get_total_price())  # Output: 100

What’s wrong here? The variable total is unnecessary because its value is directly derived from the class attributes. It adds an extra step in the method that doesn’t improve readability or efficiency.

Refactored code with query

class Order:
    def __init__(self, quantity, price):
        self.quantity = quantity
        self.price = price

    def calculate_total(self):
        return self.quantity * self.price  # Direct calculation, no temporary variable

order = Order(5, 20)
print(order.calculate_total())  # Output: 100

Why is this version better? It eliminates the unnecessary temporary variable and introduces a dedicated method (calculate_total()) to make the logic clear and reusable. If other methods need the total price, they can call calculate_total() to avoid redundant calculations. This approach enhances readability while keeping the logic encapsulated in a dedicated function.

#7 Decorator pattern

The Decorator Pattern is a powerful technique to extend the functionality of functions or classes without modifying their structure. Instead of adding logic directly inside functions, decorators let you wrap them dynamically while keeping the core logic intact.

Example without refactoring

def display_message(message):
    print(f"Displaying: {message}")

print("Before executing function")
display_message("Hello, World!")
print("After executing function")

This code directly prints statements before and after calling the function. If multiple functions need this behavior, we repeat the same logic, which violates the DRY (Don’t Repeat Yourself) principle. We can use a decorator to handle additional behaviors like logging, validation, or timing without modifying the original function. This makes the code more modular and reusable.

Refactored code using a decorator

def wrapper_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before executing function")
        result = func(*args, **kwargs)
        print("After executing function")
        return result
    return wrapper

@wrapper_decorator
def display_message(message):
    print(f"Displaying: {message}")

display_message("Hello, World!")

Here’s how it improves the code:

  • The wrapper_decorator encapsulates the extra logic.
  • The @wrapper_decorator wraps the function, so every call automatically includes pre/post-execution steps.
  • The original function remains unmodified while gaining additional behavior dynamically.

This approach keeps the code DRY and provides flexible functionality extension without modifying existing functions.

#8 Simplify function signatures

A function signature defines how a function is structured, including its name, parameters, and return type. If the signature is well-designed, it can improve code readability by ensuring proper parameter handling. Overly complex function signatures contain too many parameters or unclear return types that make the code harder to understand and modify. So, we must simplify function signatures to enhance code usability.

Example without refactoring

def process_order(order_id: int, user_id: int, product_id: int, quantity: int, 
                  discount: float, shipping_address: str, payment_method: str) -> None:
    print(f"Processing order {order_id} for user {user_id}")

Here, the function has too many parameters which make the code difficult to read and prone to errors. Managing and passing many arguments separately increases the risk of incorrect parameter ordering. We can change the situation using the data classes to simplify the function signature.

Refactored code

from dataclasses import dataclass

@dataclass
class Order:
    order_id: int
    user_id: int
    product_id: int
    quantity: int
    discount: float
    shipping_address: str
    payment_method: str

def process_order(order: Order) -> None:
    print(f"Processing order {order.order_id} for user {order.user_id}")

Why is this better? The above code encapsulates parameters inside an Order object instead of passing them individually. It reduces the function’s complexity to one parameter (order). We can now easily modify the new fields without altering the function signature.

Additionally, we can use Python’s inspect.signature() function to retrieve function signatures dynamically. This approach ensures the function signature remains clean and reduces maintenance overhead for developers.

Conclusion

In this guide, we explored the importance of Python code refactoring and the techniques that can be applied to make code more efficient and maintainable. We discussed the challenges developers face when dealing with unoptimized and hard-to-maintain code and standard refactoring techniques. These techniques help optimize the code and enhance the overall development experience. With these strategies in your toolkit, you can take your Python projects to the next level and keep the codebase flexible and easy to modify as they grow.

If you want to further sharpen your Python skills, read this blog which describes several best practices for exception handling in Python, and this blog article explains several debugging techniques which can save hours of debugging time for you.

Start to test, review and generate high quality code

Get Started

More from our blog

X