Open your project and search for a file named utils.py. Found it? And when you open it, is it packed with all kinds of "potentially useful" functions β date formatting, string processing, data validation, API wrappersβ¦
At the beginning, you probably felt clever: "Look how well I reuse code!" But a few months later, this file turns into a "minefield" no one dares to touch. New teammates can't understand it, old teammates are afraid to modify it, and everyone would rather rewrite something from scratch than dig through it to find the "right" function.
Today, let's talk about this trap that almost every Python project falls into β overusing utility functions β and share three more elegant alternatives.
01 Why Does utils.py Turn Into a Code "Dumpster"?
Let's first look at a typical scenario:
# utils.py (already grown to 800 lines)
def format_date(date_str):
"""Format date"""
# A bunch of complex logicβ¦
pass
def validate_email(email):
"""Validate email"""
# Business-specific validation rulesβ¦
pass
def calculate_discount(price, user):
"""Calculate discount"""
if user.is_vip:
return price * 0.8 # 20% off for VIP
elif user.is_new:
return price * 0.9 # 10% off for new users
return price
def process_order_data(order):
"""Process order data"""
# Mixing data cleaning and business logicβ¦
passLooks normal, right? But there are several fatal problems hidden here:
Problem 1: Loss of Context
When you call utils.validate_email(), you have no idea whether this validation rule is for user registration, email subscriptions, or a contact form. Different scenarios may require different validation rules, but utility functions lump them all together.
Problem 2: Tight Coupling Without Realizing It
calculate_discount looks like a "generic" function, but it actually hardcodes business rules (20% off for VIP, 10% off for new users). When the promotion strategy changes, you won't easily know where this function is being used, and modifying it becomes nerve-wracking.
Problem 3: A De Facto "Business Logic Dumping Ground"
Because "if you don't know where to put it, just put it in utils," this file gradually accumulates large amounts of business logic that should belong to specific modules. The result: it looks reusable, but it's actually chaotic.
Better Pattern 1
Let Behavior Live Close to Its Domain
Core idea: Put functions in the modules where they truly belong, and use clear names to tell readers what they do.
Before refactoring:
# Bad: context completely lost
from utils import format_date, validate_input
# What exactly do these functions do? Hard to tell from the names
result1 = format_date(some_date)
result2 = validate_input(some_input)After refactoring:
# Clear: each function has a well-defined home
from billing.dates import format_invoice_date
from auth.validators import validate_login_credentials
# Now it's obvious what these functions are for
invoice_date = format_invoice_date(order_date)
is_valid = validate_login_credentials(username, password)Real code example:
# Don't do this:
# utils/helpers.py
def get_user_stats(user):
"""Get user statistics"""
orders = Order.objects.filter(user=user)
total_spent = sum(order.amount for order in orders)
last_order_date = orders.last().date if orders elseNone
return {
'order_count': len(orders),
'total_spent': total_spent,
'last_order_date': last_order_date
}
# Do this instead:
# users/models.py or users/services.py
class UserStatsService:
"""Handle user statisticsβrelated logic"""
def __init__(self, user):
self.user = user
def get_stats(self):
orders = Order.objects.filter(user=self.user)
return {
'order_count': self._count_orders(orders),
'total_spent': self._calculate_total_spent(orders),
'last_order_date': self._get_last_order_date(orders)
}
def _count_orders(self, orders):
return len(orders)
def _calculate_total_spent(self, orders):
return sum(order.amount for order in orders)
def _get_last_order_date(self, orders):
return orders.last().date if orders elseNone
# Usage becomes much clear
from users.services import UserStatsService
user_stats = UserStatsService(current_user).get_stats()Better Pattern 2
When Data Needs Behavior, Use Class Methods or Properties
Core idea: If a function mainly operates on a specific type of data, it should belong to that data's class.
Before refactoring:
# utils.py
def is_order_refundable(order):
"""Check whether an order can be refunded"""
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
# Usage
from utils import is_order_refundable
if is_order_refundable(order):
process_refund(order)After refactoring:
# orders/models.py
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 an 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
# Usage - reads naturally, almost like an sentence!
if order.is_refundable:
order.process_refund()Advantages of this pattern:
- High cohesion: Order-related logic lives inside the Order class
- Discoverability: New developers only need to inspect the Order class to find related functionality
- Testability: You can test various behaviors of the Order class in isolation
- Maintainability: When refund rules change, you know exactly where to modify them
Better Pattern 3
Use Small, Focused Modules Instead of a Big All-in-One Toolbox
Core idea: Python's module system is lightweight. Don't be afraid to create small, focused modules.
Not recommended structure:
project/
βββ utils.py # 2000 lines, contains everything
βββ helpers.py # Another new pitβ¦
βββ common.py # The third dumping groundRecommended structure:
project/
βββ core/
β βββ validators.py # Dedicated to data validation
β βββ formatters.py # Dedicated to data formatting
β βββ exceptions.py # Custom exceptions
βββ billing/
β βββ calculator.py # Calculation-related
β βββ formatters.py # Billing-specific formatting
βββ users/
β βββ validators.py # User-related validation
β βββ services.py # User-related services
βββ utils/ # If it exists, only truly generic utilities
βββ date_utils.py # Pure date utilities
βββ string_utils.py # Pure string utilitiesReal example β layered organization of validation logic:
# Don't cram all validations into a single file
# utils/validation.py β
# Instead, separate them by domain and layer
# core/validators.py - Truly universal validators
def validate_email_format(email: str) -> bool:
"""Validate email format (pure technical validation)"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
# users/validators.py - User domain-specific validations
class UserValidator:
"""Business validations related to users"""
def __init__(self, user_repository):
self.user_repository = user_repository
def validate_for_registration(self, email: str, username: str):
"""Validations exclusively for registration"""
errors = []
# Format validation (call universal validator)
if not validate_email_format(email):
errors.append("Invalid email format")
# Business rule validation
if self.user_repository.email_exists(email):
errors.append("Email has already been registered")
if len(username) < 3:
errors.append("Username must be at least 3 characters long")
return errors
# orders/validators.py - Order domain-specific validations
class OrderValidator:
"""Business validations related to orders"""
def validate_for_checkout(self, order):
"""Validations before checkout"""
errors = []
if not order.items:
errors.append("Order cannot be empty")
if order.total_amount <= 0:
errors.append("Order amount must be greater than 0")
# More complex business rules...
if order.customer.has_unpaid_orders():
errors.append("You have unpaid orders, please settle them first")
return errorsSo, When Is It Okay to Use Utility Functions?
This doesn't mean you can never use utility functions. Truly generic, stateless, business-agnostic functions can still live in utility modules:
# utils/date_utils.py - This is acceptable
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 time duration (pure function)"""
hours = seconds // 3600
minutes = (seconds % 3600) // 60
return f"{hours}h {minutes}m"
# utils/string_utils.py - This is also acceptable
def truncate(text, length, suffix="..."):
"""Truncate a string (pure function)"""
if len(text) <= length:
return text
return text[:length - len(suffix)] + suffixRule of thumb: If you can take this function into a completely different project and it still works perfectly, then it's probably a true utility function.
A Simple Decision Flow
Next time you're about to write a utility function, try this decision process:

Final Thoughts
Utility functions are like "instant noodles" in programming β fast and convenient, but living on them long-term leads to malnutrition. They may make today's coding a bit faster, but they make tomorrow's maintenance much harder.
Good code design is like good city planning: related things are placed together, every road has a clear destination, and every building has a clear purpose. utils.pyturns into a dumping ground because we use it to avoid the harder question: "Where does this logic truly belong?"
Remember these three principles:
- Let behavior live close to the data it belongs to
- Use a clear module structure instead of a vague toolbox
- Only truly generic code is a utility; business-related logic must have a home
Starting today, open your project, find that bloated utils.py, and start putting it on a "diet." You'll be surprised how much readability and maintainability improve.