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…
  pass

Looks 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 ground

Recommended 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 utilities

Real 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 errors

So, 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)] + suffix

Rule 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:

None

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.