Mastering the Trade-offs of Software Coupling: A Python Refactoring Guide

Overview

Coupling represents the degree of interdependence between software modules. While popular wisdom often dictates that "coupling is bad," the reality is more nuanced. You cannot build a functional system without some level of connectivity between components, especially when integrating with external

or libraries. The goal of a professional developer isn't to eliminate coupling entirely, but to manage it through intentional design. This guide explores how to identify toxic coupling patterns and refactor them into more maintainable structures.

Prerequisites

To follow this tutorial, you should have a solid grasp of

fundamentals, including functions, classes, and type hinting. Familiarity with
Object-Oriented Programming
principles and
Data Classes
will help you understand the refactoring choices.

Key Libraries & Tools

  • Python Standard Library: Specifically the dataclasses module for cleaner object structures.
  • Typing Module: Utilizing Protocol for structural subtyping and abstraction.

Code Walkthrough: Moving Beyond Global Coupling

Global coupling occurs when functions directly access variables defined in the global scope. This makes testing and reuse nearly impossible because the function has hidden dependencies.

# Bad: Global Coupling
API_TOKEN = "secret_123"

def make_request(endpoint: str):
    # Directly accessing global constants
    return f"Calling {endpoint} with {API_TOKEN}"

A better approach groups related data into a class. This encapsulates the configuration and allows you to instantiate multiple clients with different credentials.

from dataclasses import dataclass

@dataclass
class APIClient:
    token: str
    url: str

    def make_request(self, endpoint: str):
        return f"Calling {self.url}/{endpoint} with {self.token}"

Solving Stamp Coupling with Protocols

Stamp coupling happens when you pass a large, complex object to a function that only needs a small portion of its data. This creates a brittle dependency on the entire object structure. Instead of passing the whole object, we can use a Protocol to define exactly what the function requires.

from typing import Protocol

class Loggable(Protocol):
    transaction_id: int
    amount: float

def log_transaction(item: Loggable):
    print(f"Processing ID: {item.transaction_id} for {item.amount}")

By using a protocol, the log_transaction function can now accept any object that has an ID and an amount, decoupling it from a specific Transaction class.

Syntax Notes

Python's Structural Subtyping (via Protocol) allows for "static duck typing." Unlike standard inheritance, a class does not need to explicitly inherit from a protocol to satisfy it; it simply needs to implement the required attributes or methods. This keeps your class hierarchies flat and flexible.

Practical Examples

These techniques are vital when building SDK layers or Financial Systems. In a banking app, separating "safe" withdrawals (which check balances) from "unsafe" internal transfers (which might allow temporary negatives) prevents logic duplication while maintaining clear boundaries.

Tips & Gotchas

  • Avoid "God Classes": When refactoring content coupling, don't just shove every function into a single class. This leads to bloated objects that are hard to maintain.
  • Naming Matters: If you must expose an internal modification method, name it explicitly (e.g., withdraw_unsafe) to warn future developers of the risks.
  • AI Limitations: While tools like
    GitHub Copilot
    generate code quickly, they often struggle with high-level architectural reasoning. Design patterns remain a human-led discipline.
3 min read