Stop Using a Monolithic utils.py: 3 Design Patterns to Make Your Python Code Cleaner
The article explains why a sprawling utils.py becomes a maintenance nightmare in Python projects, outlines three concrete patterns—module‑level placement, class‑based methods, and small dedicated modules—to restructure code, and provides decision criteria for when a function truly belongs in a utility module.
Many Python projects accumulate a huge utils.py file that ends up as a “garbage dump” because developers keep adding unrelated helper functions. The article first shows a typical 800‑line utils.py containing date formatting, string handling, data validation, and API wrappers, and explains three hidden problems:
Problem 1: Loss of Context
Calling utils.validate_email() gives no clue whether the validation is for registration, subscription, or a contact form, mixing different scenarios in one place.
Problem 2: Tight Coupling without Awareness
The function calculate_discount hard‑codes business rules (VIP 20% off, new user 10% off). When the promotion changes, developers must hunt down every usage, making changes risky.
Problem 3: Business‑Logic Garbage Dump
Because “if you don’t know where to put it, put it in utils”, domain‑specific logic accumulates, turning apparent reuse into chaos.
Better Pattern 1 – Put Functions in Their Own Modules
Let behavior live close to its domain.
Before refactor:
from utils import format_date, validate_input
result1 = format_date(some_date)
result2 = validate_input(some_input)After refactor:
from billing.dates import format_invoice_date
from auth.validators import validate_login_credentials
invoice_date = format_invoice_date(order_date)
is_valid = validate_login_credentials(username, password)Each function now has a clear ownership and a descriptive name.
Better Pattern 2 – Use Class Methods or Properties for Data‑Specific Behavior
When data needs behavior, attach it to the data’s class.
Before refactor:
def is_order_refundable(order):
if order.status != 'completed':
return False
if order.created_at < datetime.now() - timedelta(days=30):
return False
if order.has_refund_request:
return False
return True
if is_order_refundable(order):
process_refund(order)After refactor:
class Order:
def __init__(self, status, created_at, has_refund_request=False):
self.status = status
self.created_at = created_at
self.has_refund_request = has_refund_request
@property
def is_refundable(self):
"""Check whether the order can be refunded"""
if self.status != 'completed':
return False
if self.created_at < datetime.now() - timedelta(days=30):
return False
if self.has_refund_request:
return False
return True
if order.is_refundable:
order.process_refund()This keeps the refund logic encapsulated in the Order class, improving cohesion and testability.
Better Pattern 3 – Small, Focused Modules Instead of a Big Toolbox
Use tiny, purpose‑built modules rather than a monolithic utils package.
Bad structure:
project/
├── utils.py # 2000 lines, everything
├── helpers.py # another catch‑all
└── common.py # third dumpRecommended structure:
project/
├── core/
│ ├── validators.py # data validation
│ ├── formatters.py # data formatting
│ └── exceptions.py # custom exceptions
├── billing/
│ ├── calculator.py # billing calculations
│ └── formatters.py # billing‑specific formatting
├── users/
│ ├── validators.py # user‑related validation
│ └── services.py # user services
└── utils/
├── date_utils.py # pure date helpers
└── string_utils.py # pure string helpersOnly truly generic, stateless functions belong in utils (e.g., days_between, format_duration, truncate).
When Is a Utility Function Appropriate?
A function can stay in a utility module if it is genuinely generic, stateless, and unrelated to business rules. Example:
# utils/date_utils.py
def days_between(date1, date2):
"""Calculate the number of days between two dates (pure function)"""
return abs((date2 - date1).days)
def format_duration(seconds):
"""Format a time interval (pure function)"""
hours = seconds // 3600
minutes = (seconds % 3600) // 60
return f"{hours}h {minutes}m"
# utils/string_utils.py
def truncate(text, length, suffix="..."):
"""Truncate a string (pure function)"""
if len(text) <= length:
return text
return text[:length - len(suffix)] + suffixDecision rule: if the function works unchanged in a completely different project, it is likely a true utility.
Simple Decision Flowchart
Start
↓
Is the function mainly operating on a specific object?
↓
Yes → Make it a method/property (Pattern 2)
No
↓
Does the function belong to a particular business domain?
↓
Yes → Place it in the corresponding domain module (Pattern 1)
No
↓
Is the function completely generic, stateless, and business‑agnostic?
↓
Yes → Put it in a small dedicated utility module (Pattern 3)
No → Re‑think whether the function should exist.Final Thoughts
Utility functions are like instant noodles—quick and convenient but nutritionally poor if overused. Good code design resembles city planning: related pieces stay together, roads have clear destinations, and each building has a defined purpose. By applying the three principles—keep behavior near its data, replace vague toolboxes with clear module structures, and reserve utilities for truly generic code—you can dramatically improve readability and maintainability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Data STUDIO
Click to receive the "Python Study Handbook"; reply "benefit" in the chat to get it. Data STUDIO focuses on original data science articles, centered on Python, covering machine learning, data analysis, visualization, MySQL and other practical knowledge and project case studies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
