Code Smells: Common Signs of Poor Code Quality
Code smells are surface-level symptoms in source code that usually indicate deeper problems — they are not bugs themselves but warning signs that something may be wrong with the design or maintainability of your software.
What You’ll Learn
- How to identify the six most common code smells
- What causes each smell and what deeper problem it indicates
- How to fix each smell with concrete refactoring examples
- When a code smell is acceptable versus a real problem
Why Code Smells Matter
Code smells make software harder to understand, harder to change, and harder to debug. A codebase with many smells has higher defect rates, slower feature delivery, and longer onboarding times. Recognizing smells is the first step toward writing cleaner, more maintainable code.
Durga Antivirus Pro uses automated code smell detection in its CI pipeline to flag potential issues in malware signature processing code before they reach production.
Learning Path
flowchart LR
A[Testing Basics] --> B[Code Smells<br/>You are here]
B --> C[Refactoring Techniques]
C --> D[Design Patterns]
D --> E[Clean Architecture]
style B fill:#f90,color:#fff
Long Methods
Methods that try to do too much. A method should do one thing and do it well.
Smell signals: The method spans more than 20-30 lines, has multiple levels of indentation, or requires scrolling to read fully.
# BAD: Long method doing everything
def process_order(order, user, inventory, payment):
# Validate order
if not order.items:
raise ValueError("Empty order")
if not user.is_active:
raise ValueError("Inactive user")
# Check inventory
for item in order.items:
if inventory.get(item.id).stock < item.quantity:
raise ValueError(f"Insufficient stock: {item.id}")
# Process payment
charge = payment.charge(user.card, order.total)
if not charge.success:
raise ValueError("Payment failed")
# Update inventory
for item in order.items:
inventory.decrement(item.id, item.quantity)
# Create shipment
shipment = Shipment.create(order, user.address)
return shipment
# GOOD: Extracted into focused methods
def process_order(order, user, inventory, payment):
validate_order(order, user)
check_inventory(order, inventory)
process_payment(order, user, payment)
update_inventory(order, inventory)
return create_shipment(order, user)Large Classes
Classes that accumulate too many responsibilities. A large class often violates the Single Responsibility Principle.
Smell signals: More than 200-300 lines, 20+ methods, or fields that could be grouped into separate concerns.
# BAD: God class with multiple responsibilities
class UserManager:
def create_user(self, email, password): ...
def send_email(self, subject, body): ...
def generate_report(self, user_id): ...
def log_activity(self, action): ...
def calculate_metrics(self): ...
# GOOD: Separated into focused classes
class UserService:
def create_user(self, email, password): ...
class EmailService:
def send_email(self, subject, body): ...
class ReportGenerator:
def generate_report(self, user_id): ...
class ActivityLogger:
def log_activity(self, action): ...Feature Envy
A method that seems more interested in another class’s data than its own. It accesses more data from other objects than from itself.
Smell signals: Methods that call many getters on another object and perform calculations with that data.
# BAD: Feature envy — OrderSummary obsesses over Customer
class OrderSummary:
def generate(self, customer):
name = customer.first_name + " " + customer.last_name
total = sum(item.price for item in customer.orders)
discount = customer.loyalty_points * 0.01
return f"Customer: {name}, Total: {total - discount}"
# GOOD: Let Customer own its data
class Customer:
def full_name(self):
return f"{self.first_name} {self.last_name}"
def discounted_total(self):
total = sum(item.price for item in self.orders)
discount = self.loyalty_points * 0.01
return total - discount
class OrderSummary:
def generate(self, customer):
return f"Customer: {customer.full_name()}, Total: {customer.discounted_total()}"Shotgun Surgery
A change that requires modifying many different files or classes. The opposite of a “single, clear place to change.”
Smell signals: A single feature change touches 5+ files. Adding a new field requires updating database schema, API layer, service layer, UI, and tests.
# BAD: Adding a field requires changes everywhere
# models/user.py
class User:
def __init__(self, name, email):
self.name = name
self.email = email
# serializers/user.py
class UserSerializer:
def serialize(self, user):
return {"name": user.name, "email": user.email}
# validators/user.py
def validate_user(user):
if not user.name: raise Error
if not user.email: raise Error
# GOOD: Centralize the field definition using a dataclass
from dataclasses import dataclass
@dataclass
class User:
name: str
email: str
# Add new fields here onlyPrimitive Obsession
Using primitive types (strings, integers, booleans) instead of small objects for domain concepts like email addresses, phone numbers, currency amounts, or ZIP codes.
Smell signals: Validation logic for the same primitive value scattered across the codebase.
# BAD: Primitive obsession
def send_email(to_address: str, subject: str, body: str):
if "@" not in to_address:
raise ValueError("Invalid email")
# ... send
def register_user(email: str):
if "@" not in email:
raise ValueError("Invalid email")
# ... register
# GOOD: Value object
class Email:
def __init__(self, address: str):
if "@" not in address:
raise ValueError(f"Invalid email: {address}")
self.address = address
def send_email(to: Email, subject: str, body: str): ...
def register_user(email: Email): ...Data Clumps
Groups of data items that appear together repeatedly — like (street, city, ZIP) or (width, height). These should be extracted into their own class.
Smell signals: The same 3+ fields appearing in multiple method signatures or classes.
# BAD: Repeated data clumps
def calculate_shipping(street, city, zip_code, country): ...
def validate_address(street, city, zip_code, country): ...
def format_label(street, city, zip_code, country): ...
# GOOD: Extract into a value object
@dataclass
class Address:
street: str
city: str
zip_code: str
country: str
def calculate_shipping(address: Address): ...
def validate_address(address: Address): ...
def format_label(address: Address): ...Common Errors
1. Fixing Smells Prematurely
Not every smell needs fixing. A long method in a rarely-changed utility script is fine. Prioritize smells in code that changes frequently.
2. Over-Engineering to Avoid Smells
Breaking a 50-line class into 10 files of 5 lines each is worse. Refactoring should simplify, not fragment.
3. Ignoring Testability
Code that’s hard to test almost always has design smells. Use test difficulty as a smell detector.
4. Smell Blindness
Teams that have lived with smells for years stop noticing them. Pair code reviews and fresh eyes help.
5. Treating Symptoms Not Causes
A smell is a symptom. Fix the underlying design problem, not just the surface-level issue.
6. Not Using Automated Detectors
Tools like PyLint, ESLint, and SonarQube detect many smells automatically. Run them in CI.
Practice Questions
What is a code smell? A surface-level symptom that usually indicates a deeper design problem.
What is Feature Envy? When a method shows more interest in another class’s data than its own.
What does Shotgun Surgery mean? A single change requiring modifications in many different files.
What is Primitive Obsession? Using primitive types instead of small value objects for domain concepts.
How do you fix a Data Clump? Extract the repeated group of fields into its own class or value object.
Challenge: Run a linter on your project with code smell detection enabled. Document the top 10 smells found, classify each by type, and fix the three most critical ones. Measure the improvement in cyclomatic complexity.
FAQ
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| Code Refactoring Techniques | Systematic approaches to fixing code smells |
| Technical Debt Management | How code smells contribute to technical debt |
| Static Code Analysis Tools | Automating code smell detection |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-19.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro