Functions are an essential aspect of Python programming and act as the foundational building blocks for all software programs. Their primary objective is to execute precise tasks while simultaneously enhancing code organization by creating reusable segments. This organization greatly enhances the readability and maintainability of code.
In this blog post, I’ll provide some useful tips and tricks that help enhance the readability, coherence, and maintainability of your code.
Avoid monolithic design
import datetime
from dataclasses import dataclass
@dataclass
class Card:
name: str
number: str
exp_date: str
cvv: int
balance: int = 0
class Cart:
def __init__(self, items: list[dict]) -> None:
self.items = items
@property
def total(self) -> int:
return sum(item["price"] * item["quantity"] for item in self.items)
def checkout(card: Card, cart: Cart) -> bool:
def digits_of(number: str) -> list[int]:
return [int(d) for d in number]
digits = digits_of(card.number)
odd_digits = digits[-1::-2]
even_digits = digits[-2::-2]
checksum = 0
checksum += sum(odd_digits)
for digit in even_digits:
checksum += sum(digits_of(str(digit * 2)))
if not checksum % 10 == 0:
raise ValueError("Invalid card number")
card_exp_date = datetime.datetime.strptime(card.exp_date, "%m/%y")
if card_exp_date < datetime.datetime.now():
raise ValueError("Card has expired")
if not 100 <= card.cvv <= 999:
raise ValueError("Invalid CVV")
if len(cart.items) == 0:
raise ValueError("Empty cart")
total = sum(item["price"] * item["quantity"] for item in cart.items)
if total == 0:
raise ValueError("Empty cart")
if total > card.balance:
raise ValueError("Insufficient funds")
card.balance -= total
print(f"Charged {total} to card {card.number}")
def main():
card = Card("John Doe", "4242424242424242", "01/26", 123, 100)
cart = Cart([{"name": "apple", "price": 1, "quantity": 1}])
try:
checkout(card, cart)
except ValueError as e:
print(e)
else:
print("Success")
if __name__ == "__main__":
main()
The checkout function is overly complex and performs multiple tasks; this is an example of monolithic design. This presents significant challenges for testing and lacks reusability. To address these issues, the best approach would involve breaking down the function into smaller, specialized functions. This would aid in readability, testing, and reusability.
def luhn_checksum(card_number: str) -> bool:
def digits_of(number: str) -> list[int]:
return [int(d) for d in number]
digits = digits_of(card_number)
odd_digits = digits[-1::-2]
even_digits = digits[-2::-2]
checksum = 0
checksum += sum(odd_digits)
for digit in even_digits:
checksum += sum(digits_of(str(digit * 2)))
return checksum % 10 == 0
def validate_card(card: Card) -> None:
if not luhn_checksum(card.number):
raise ValueError("Invalid card number")
card_exp_date = datetime.datetime.strptime(card.exp_date, "%m/%y")
if card_exp_date < datetime.datetime.now():
raise ValueError("Card has expired")
if not 100 <= card.cvv <= 999:
raise ValueError("Invalid CVV")
def validate_cart(cart: Cart) -> None:
if len(cart.items) == 0:
raise ValueError("Empty cart")
total = sum(item["price"] * item["quantity"] for item in cart.items)
if total == 0:
raise ValueError("Empty cart")
def charge_card(card: Card, cart: Cart) -> None:
total = cart.total
if total > card.balance:
raise ValueError("Insufficient funds")
card.balance -= total
print(f"Charged {total} to card {card.number}")
def checkout(card: Card, cart: Cart) -> None:
validate_card(card)
validate_cart(cart)
charge_card(card, cart)
The singular modification we implemented has given us considerable flexibility. By enabling the capability to evaluate each function, we have opened a pathway to more granular control and customization. This is particularly advantageous when integrating these functions into different sections of our program, as it allows for a more modular and scalable architecture. The independence of function evaluation not only simplifies debugging and maintenance but also enhances the overall readability and efficiency of our code. And due to the independent nature of our functions, they are now much easier to test.
Do one thing and do it well
In development, functions must have a clear purpose, aiming for excellence in their specific area of responsibility. This principle ensures that code stays clean, understandable, and maintainable. When functions have a single objective, it simplifies debugging, enhances code readability, and enables easy modifications. By following this guideline, developers can create a structured and efficient codebase.
def validate_card_number(card_number: str) -> None:
if not luhn_checksum(card_number):
raise ValueError("Invalid card number")
def validate_exp_date(card: Card) -> None:
card_exp_date = datetime.datetime.strptime(card.exp_date, "%m/%y")
if card_exp_date < datetime.datetime.now():
raise ValueError("Card has expired")
def validate_cvv(card: Card) -> None:
if not 100 <= card.cvv <= 999:
raise ValueError("Invalid CVV")
def validate_funds(card: Card, cart: Cart) -> None:
if cart.total > card.balance:
raise ValueError("Insufficient funds")
def validate_card(card: Card, cart: Cart) -> None:
validate_card_number(card.number)
validate_exp_date(card)
validate_cvv(card)
validate_funds(card, cart)
def validate_cart(cart: Cart) -> None:
if cart.total == 0:
raise ValueError("Empty cart")
def charge_card(card: Card, cart: Cart) -> None:
card.balance -= cart.total
print(f"Charged {cart.total} to card {card.number}")
def checkout(card: Card, cart: Cart) -> None:
validate_cart(cart)
validate_card(card, cart)
charge_card(card, cart)
We have now organized our code into smaller functions, with each one dedicated to a specific task. This improvement promotes the ability to conduct tests more effectively and encourages code reuse. Additionally, this coding approach aligns with the principles of single responsibility and allows for extendability without requiring modifications.
Breaking down code into smaller, more focused functions enhances maintainability and adaptability. This method is especially effective for managing complex systems and allows for isolated understanding, testing, and modification of each function. It simplifies debugging and development while facilitating collaboration among developers, streamlining the process. Ultimately, this strategy embodies efficient, maintainable, and scalable code design.
Requesting only the necessary data
Despite notable enhancements in these functions, there remains a substantial challenge with data transfer, particularly the transmission of complete objects. In many scenarios, it’s more efficient to pass only specific attributes of these objects rather than the entire instance. This approach is not only more streamlined but also crucial for optimizing testability. When functions are tightly coupled with complete objects, it can hinder the flexibility and maintainability of the code. By focusing on passing only necessary attributes, we can reduce overhead and enhance the separation of concerns, leading to cleaner, more manageable code. This method also facilitates easier testing, as it allows for more focused and isolated testing scenarios.
def validate_card_number(card_number: str) -> None:
if not luhn_checksum(card_number):
raise ValueError("Invalid card number")
def validate_exp_date(exp_date: str) -> None:
card_exp_date = datetime.datetime.strptime(exp_date, "%m/%y")
if card_exp_date < datetime.datetime.now():
raise ValueError("Card has expired")
def validate_cvv(cvv: int) -> None:
if not 100 <= cvv <= 999:
raise ValueError("Invalid CVV")
def validate_funds(balance: int, total: int) -> None:
if total > balance:
raise ValueError("Insufficient funds")
def validate_card(card:Card, total: int) -> None:
validate_card_number(card.number)
validate_exp_date(card.exp_date)
validate_cvv(card.cvv)
validate_funds(card.balance, total)
def validate_cart(total: int) -> None:
if total == 0:
raise ValueError("Empty cart")
def charge_card(card: Card, total: int) -> None:
card.balance -= total
print(f"Charged {total} to card {card.number}")
def checkout(card: Card, cart: Cart) -> None:
validate_cart(cart.total)
validate_card(card, cart.total)
charge_card(card, cart.total)
At present, we share only essential data, enhancing flexibility, reusability, and performance. Transmitting only crucial information minimizes data overhead, improving code efficiency and software architecture. This modular design allows functions to operate effectively with minimal and necessary information, promoting maintainability. Practical adaptability facilitates integration and scalability, adding to system robustness.
Functions as first-class objects
Recognizing functions as objects that can be passed as arguments to other functions enhances their functionality and adaptability. By passing validators and chargers as arguments to the checkout function, we enable various implementations. This simplifies the code structure and increases flexibility, as these components can be effortlessly swapped or updated without altering the core checkout function. Treating functions as first-class objects utilizes the capabilities of higher-order functions, leading to a more dynamic and modular codebase.
from typing import Callable
def checkout(card: Card, cart: Cart, card_validator: Callable, cart_validator: Callable, card_charger: Callable) -> None:
cart_validator(cart.total)
card_validator(card, cart.total)
card_charger(card, cart.total)
def main():
card = Card("John Doe", "4242424242424242", "01/26", 123, 100)
cart = Cart([{"name": "apple", "price": 1, "quantity": 1}])
checkout(card, cart, validate_card, validate_cart, charge_card)
if __name__ == "__main__":
main()
Final thoughts
In order to become proficient in Python functions, it’s essential to go beyond basic coding. This requires creating reusable, modular, and efficient components. By focusing on simplicity, handling one task at a time, and obtaining only the necessary data, we can achieve a high level of coding expertise and effectiveness.
When incorporating these concepts into your Python projects, strive for functionality, cleanliness, comprehensibility, and ease of maintenance. Python’s simplicity and power become evident when we embrace these practices, enabling the development of robust, scalable, and efficient applications.