Table of Contents
- Misusing Inheritance: Over-Inheritance and Fragile Base Classes
- Ignoring “Composition Over Inheritance”
- Poor Encapsulation: Exposing Internal State
- Not Using Abstract Base Classes (ABCs) for Interface Enforcement
- Mutable Default Arguments in Methods
- Violating the Single Responsibility Principle (SRP)
- Inconsistent Method Signatures in Inheritance
- Overusing Class Attributes
- Neglecting Exception Handling in OOP
- Lack of Unit Tests for OOP Components
1. Misusing Inheritance: Over-Inheritance and Fragile Base Classes
Mistake Description
Inheritance is often overused to reuse code, leading to deep, rigid hierarchies. A common anti-pattern is the “fragile base class” problem: subclasses depend on implementation details of parent classes, so small changes to the parent break all subclasses.
Why It Causes Technical Debt
- Rigidity: Hierarchies become hard to modify as they grow.
- Fragility: Subclasses are tightly coupled to parent logic, making refactoring risky.
- Confusion: “Is-a” relationships are forced where “has-a” (composition) would be clearer.
Example: Bad Inheritance
class Engine:
def start(self):
print("Engine starting...")
# Forcing "Car is-a Engine" (incorrect "is-a" relationship)
class Car(Engine):
def drive(self):
self.start() # Depends on Engine's start() implementation
print("Car driving...")
# If Engine's start() changes (e.g., adds a parameter), Car breaks!
Fixed Example: Composition Over Inheritance
class Engine:
def start(self):
print("Engine starting...")
# "Car has-a Engine" (correct "has-a" relationship)
class Car:
def __init__(self):
self.engine = Engine() # Compose with Engine
def drive(self):
self.engine.start() # Uses Engine's interface, not implementation
print("Car driving...")
Key Takeaways
- Use inheritance only for true “is-a” relationships (e.g.,
Dogis-aAnimal). - Prefer composition (
has-a) to reuse behavior (e.g.,Carhas-aEngine).
2. Ignoring “Composition Over Inheritance”
Mistake Description
Developers often default to inheritance for code reuse, even when composition is more flexible. This leads to bloated base classes trying to handle every edge case.
Why It Causes Technical Debt
- Bloat: Base classes accumulate unrelated methods to support all subclasses.
- Inflexibility: Adding new behaviors requires modifying the hierarchy.
Example: Over-Inherited Report Generator
# Bloated base class trying to handle all formats
class Report:
def generate_pdf(self):
print("Generating PDF...")
def generate_csv(self):
print("Generating CSV...")
# Subclasses inherit unused methods (e.g., PDFReport has generate_csv)
class PDFReport(Report):
pass # Only needs generate_pdf, but inherits generate_csv
class CSVReport(Report):
pass # Only needs generate_csv, but inherits generate_pdf
Fixed Example: Composed Formatters
# Small, focused formatter classes
class PDFFormatter:
def generate(self):
print("Generating PDF...")
class CSVFormatter:
def generate(self):
print("Generating CSV...")
# Report composes with a formatter
class Report:
def __init__(self, formatter):
self.formatter = formatter # Inject formatter
def generate(self):
self.formatter.generate() # Delegate to formatter
# Flexible: New formats (e.g., Excel) don't require changing Report
pdf_report = Report(PDFFormatter())
csv_report = Report(CSVFormatter())
Key Takeaways
- Compose with “behavior objects” (e.g.,
Formatter) to add flexibility. - Avoid “god classes” that do everything; split logic into smaller components.
3. Poor Encapsulation: Exposing Internal State
Mistake Description
Failing to hide internal state (e.g., using public instance variables) allows external code to modify objects in unexpected ways, breaking invariants.
Why It Causes Technical Debt
- Fragility: External code depends on internal variables, making refactoring hard.
- Inconsistency: No validation when state is modified (e.g., a negative bank balance).
Example: Exposed Bank Account
class BankAccount:
def __init__(self, balance):
self.balance = balance # Public! Anyone can modify directly
# External code can break invariants (e.g., negative balance)
account = BankAccount(100)
account.balance = -500 # No validation!
Fixed Example: Encapsulated with Properties
class BankAccount:
def __init__(self, balance):
self._balance = balance # "Private" by convention (single underscore)
@property
def balance(self):
return self._balance # Read-only access
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
# Now state changes are controlled and validated
account = BankAccount(100)
account.withdraw(50) # Valid: balance becomes 50
account.balance = -500 # Error: can't modify directly
Key Takeaways
- Use “private” variables (conventionally
_variable) to hide state. - Expose controlled access via methods/properties with validation.
4. Not Using Abstract Base Classes (ABCs) for Interface Enforcement
Mistake Description
When designing class hierarchies, failing to enforce required methods (interfaces) leads to subclasses that break expectations (e.g., a Shape subclass without an area() method).
Why It Causes Technical Debt
- Silent Failures: Bugs occur at runtime when methods are missing, not at development time.
- Ambiguity: No clear contract for what subclasses must implement.
Example: Unenforced Shape Interface
class Shape:
# No enforcement of required methods
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Implements area()
return 3.14 * self.radius **2
class Square(Shape):
def __init__(self, side):
self.side = side
# Oops! Forgot to implement area()
# Runtime error when using Square (no area method)
shapes = [Circle(2), Square(3)]
for shape in shapes:
print(shape.area()) # AttributeError: 'Square' object has no attribute 'area'
Fixed Example: ABC with Abstract Methods
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract base class
@abstractmethod # Enforce subclasses to implement area()
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Must implement area()
return 3.14 * self.radius** 2
class Square(Shape):
def __init__(self, side):
self.side = side
# Now required to implement area()—fails at *class definition* if missing!
def area(self):
return self.side **2
# No runtime errors—interface is enforced
shapes = [Circle(2), Square(3)]
for shape in shapes:
print(shape.area()) # Works: 12.56, 9
Key Takeaways
- Use
abc.ABCand@abstractmethodto define interfaces. - Catch missing methods at development time, not runtime.
5. Mutable Default Arguments in Methods
Mistake Description
Python initializes default arguments once when the function is defined, not on each call. Using mutable defaults (e.g., [], {}) leads to unexpected state retention between calls.
Why It Causes Technical Debt
- Hidden State: Methods share the same mutable default across instances/calls.
- Bugs: Hard-to-debug issues from unintended data leakage.
Example: Mutable Default Disaster
class ShoppingCart:
# Mutable default: [] is initialized once, shared across all calls
def add_item(self, item, items=[]):
items.append(item)
return items
cart1 = ShoppingCart()
cart1.add_item("apple") # items = ["apple"]
cart1.add_item("banana") # items = ["apple", "banana"]
cart2 = ShoppingCart()
cart2.add_item("orange") # Oh no! items = ["apple", "banana", "orange"] (shared!)
Fixed Example: Immutable Default + Fresh Initialization
class ShoppingCart:
# Use None (immutable) as default
def add_item(self, item, items=None):
items = items or [] # Initialize fresh list on each call
items.append(item)
return items
cart1 = ShoppingCart()
cart1.add_item("apple") # ["apple"]
cart1.add_item("banana") # ["banana"] (new list each time)
cart2 = ShoppingCart()
cart2.add_item("orange") # ["orange"] (no leakage!)
Key Takeaways
- Never use mutable objects (
[],{},set()) as default arguments. - Use
Noneand initialize mutable values inside the method.
6. Violating the Single Responsibility Principle (SRP)
Mistake Description
A class承担过多责任(例如,处理数据、验证、日志记录和网络请求),导致其臃肿且难以维护。
Why It Causes Technical Debt
- Low Cohesion: Changes to one responsibility risk breaking others.
- Hard to Test: Testing requires mocking unrelated dependencies (e.g., a database and an email server).
Example: Jack-of-All-Trades User Service
class UserService:
# Violates SRP: handles DB, validation, and email
def create_user(self, name, email):
# 1. Validate email
if "@" not in email:
raise ValueError("Invalid email")
# 2. Save to database
print(f"Saving {name} to DB...")
# 3. Send welcome email
print(f"Sending email to {email}...")
Fixed Example: Split by Responsibility
class EmailValidator:
@staticmethod
def is_valid(email):
return "@" in email
class DatabaseHandler:
@staticmethod
def save_user(name):
print(f"Saving {name} to DB...")
class EmailService:
@staticmethod
def send_welcome(email):
print(f"Sending email to {email}...")
class UserService:
# Single responsibility: orchestrate user creation
def __init__(self):
self.validator = EmailValidator()
self.db = DatabaseHandler()
self.emailer = EmailService()
def create_user(self, name, email):
if not self.validator.is_valid(email):
raise ValueError("Invalid email")
self.db.save_user(name)
self.emailer.send_welcome(email)
Key Takeaways
- A class should have only one reason to change (e.g.,
EmailServicechanges only when email logic changes). - Split responsibilities into smaller, focused classes.
7. Inconsistent Method Signatures in Inheritance
Mistake Description
Overriding a parent method with a different signature (e.g., adding/removing parameters) breaks the “Liskov Substitution Principle” (LSP), making subclasses incompatible with parent expectations.
Why It Causes Technical Debt
- Broken Polymorphism: Code expecting a parent class may fail when given a subclass.
- Confusion: Developers using the subclass must check method signatures.
Example: Inconsistent Signature
class Animal:
def make_sound(self, volume=5): # Parent has volume parameter
print(f"Animal sound at volume {volume}")
class Dog(Animal):
# Override with different signature (no volume)
def make_sound(self):
print("Woof!")
# Polymorphic code expects volume parameter
animals = [Animal(), Dog()]
for animal in animals:
animal.make_sound(volume=3) # Error: Dog.make_sound() takes 0 positional arguments
Fixed Example: Consistent Signatures
class Animal:
def make_sound(self, volume=5):
print(f"Animal sound at volume {volume}")
class Dog(Animal):
# Match parent signature (accept volume, even if unused)
def make_sound(self, volume=5):
print(f"Woof! (volume {volume})")
# Polymorphic code works with all subclasses
animals = [Animal(), Dog()]
for animal in animals:
animal.make_sound(volume=3) # Works: Animal sound..., Woof! (volume 3)
Key Takeaways
- Subclass methods must match the parent’s signature (parameters, defaults).
- Use
*args/**kwargsif needed to maintain compatibility.
8. Overusing Class Attributes
Mistake Description
Class attributes are shared across all instances. Overusing them for instance-specific data leads to unexpected cross-instance interference.
Why It Causes Technical Debt
- Shared State: Instances accidentally modify each other’s data.
- Hidden Coupling: Changes to one instance affect others.
Example: Misused Class Attribute
class Counter:
count = 0 # Class attribute (shared by all instances)
def increment(self):
self.count += 1 # Modifies the *class* attribute, not instance
counter1 = Counter()
counter1.increment()
print(counter1.count) # 1
counter2 = Counter()
print(counter2.count) # 1 (unexpected! counter2 was never incremented)
Fixed Example: Instance Attributes
class Counter:
def __init__(self):
self.count = 0 # Instance attribute (per-instance data)
def increment(self):
self.count += 1
counter1 = Counter()
counter1.increment()
print(counter1.count) # 1
counter2 = Counter()
print(counter2.count) # 0 (correct: separate state)
Key Takeaways
- Use class attributes only for data shared across all instances (e.g., constants).
- Use instance attributes (
self.variable) for per-instance state.
9. Neglecting Exception Handling in OOP
Mistake Description
Poor exception handling (e.g., catching overly broad exceptions like Exception or not raising meaningful errors) hides bugs and makes debugging harder.
Why It Causes Technical Debt
- Silent Failures: Errors are suppressed instead of fixed.
- Ambiguity: Generic exceptions don’t reveal the root cause.
Example: Overly Broad Exception
class FileProcessor:
def read_file(self, path):
try:
with open(path, "r") as f:
return f.read()
except Exception: # Catches *all* exceptions (e.g., KeyboardInterrupt, OSError)
print("Something went wrong") # Hides the real error
processor = FileProcessor()
processor.read_file("nonexistent.txt") # Prints "Something went wrong" (no details)
Fixed Example: Specific Exceptions + Custom Errors
class FileProcessorError(Exception):
"""Custom exception for FileProcessor issues."""
class FileProcessor:
def read_file(self, path):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError:
raise FileProcessorError(f"File not found: {path}") from None
except PermissionError:
raise FileProcessorError(f"Permission denied for {path}") from None
# Caller gets clear, actionable errors
processor = FileProcessor()
try:
processor.read_file("nonexistent.txt")
except FileProcessorError as e:
print(e) # "File not found: nonexistent.txt" (specific error)
Key Takeaways
- Catch specific exceptions (e.g.,
FileNotFoundError), notException. - Define custom exceptions to clarify OOP-specific errors.
10. Lack of Unit Tests for OOP Components
Mistake Description
Failing to test class behavior (e.g., inheritance, encapsulation, method interactions) leads to regressions when code changes.
Why It Causes Technical Debt
- Unverified Behavior: Bugs in inheritance or state management go undetected.
- Fear of Refactoring: Developers avoid improving code for fear of breaking it.
Example: Untested Bank Account
class BankAccount:
def __init__(self, balance):
self._balance = balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
Fixed Example: Tested Bank Account
import unittest
class BankAccount:
def __init__(self, balance):
self._balance = balance
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
return self._balance
class TestBankAccount(unittest.TestCase):
def test_withdraw_success(self):
account = BankAccount(100)
self.assertEqual(account.withdraw(50), 50)
def test_withdraw_insufficient_funds(self):
account = BankAccount(100)
with self.assertRaises(ValueError):
account.withdraw(200)
# Tests ensure behavior works as expected, even after refactoring
if __name__ == "__main__":
unittest.main()
Key Takeaways
- Test methods, inheritance, encapsulation (e.g., can’t set invalid state).
- Use tests to validate OOP contracts (e.g., “withdraw raises error on insufficient funds”).
Conclusion
Avoiding these Python OOP mistakes is critical to writing maintainable, debt-free code. By prioritizing composition over inheritance, enforcing interfaces with ABCs, encapsulating state, and testing rigorously, you’ll build systems that are flexible, resilient, and easy to extend.
Technical debt isn’t just about bad code—it’s about the time and effort wasted fixing preventable issues. Invest in clean OOP practices today to save countless hours tomorrow.