low-class-cohesion¶
Category: Class Design
Severity: Configurable by threshold
Triggered by: pyscn analyze, pyscn check
What it does¶
Flags classes whose methods don't share instance state (the LCOM4 metric — Lack of Cohesion of Methods, version 4). pyscn builds a graph where two methods are connected if they touch a common self. attribute, then counts the connected components. LCOM4 = 1 means every method is related to every other; LCOM4 = N means the class is really N unrelated sub-classes glued together.
Methods decorated with @staticmethod, @classmethod, or @abstractmethod do not reference instance state and are excluded from the graph.
Constructors (__init__, __new__, and the dataclass __post_init__ hook) are excluded too. A constructor typically initializes every attribute of the class, so keeping it in the graph links all of the responsibility clusters it sets up and collapses LCOM4 to 1 for almost any class. The attributes it introduces still count toward instance_variables.
Methods that touch no instance state and are not connected to any other method by a call are left out of the graph too. They cannot be split from anything, so counting them would only inflate LCOM4.
In a class with a base class, self.name() is treated as a call to an inherited method, not as instance state, when the class only ever calls name and never assigns or defines it. That makes it the same call as super().name(), and the two spellings get the same result.
In plain terms: this class is doing unrelated jobs — split it, or make it a module of functions.
Why is this a problem?¶
A class is meant to bundle state with the operations that act on it. When the methods don't touch the same state:
- The class name lies — it claims to be one thing but behaves like two or three.
- Changes scatter — a bug in one responsibility can only be found by reading code that has nothing to do with it.
- Reuse is blocked — you cannot lift out the part you need without dragging the rest along.
- It often predates a real abstraction — the "Utilities" or "Manager" class is a classic symptom.
Example¶
class UserUtility:
def __init__(self, db, smtp, clock):
self.db = db
self.smtp = smtp
self.clock = clock
self.cache = {}
# --- persistence ---
def load(self, user_id):
if user_id in self.cache:
return self.cache[user_id]
row = self.db.fetch("users", user_id)
self.cache[user_id] = row
return row
def save(self, user):
self.db.upsert("users", user)
self.cache[user.id] = user
# --- email ---
def send_welcome(self, address):
self.smtp.send(address, "Welcome")
def send_reset(self, address, token):
self.smtp.send(address, f"Reset: {token}")
# --- formatting ---
def format_joined_at(self, user):
return self.clock.format(user.joined_at)
LCOM4 = 3: {load, save} share db and cache, {send_welcome, send_reset} share smtp, {format_joined_at} stands alone. Three components, one class.
Use instead¶
Split into cohesive classes, and move the stateless part out to free functions.
class UserRepository:
def __init__(self, db):
self._db = db
self._cache = {}
def load(self, user_id):
if user_id in self._cache:
return self._cache[user_id]
row = self._db.fetch("users", user_id)
self._cache[user_id] = row
return row
def save(self, user):
self._db.upsert("users", user)
self._cache[user.id] = user
class UserMailer:
def __init__(self, smtp):
self._smtp = smtp
def send_welcome(self, address):
self._smtp.send(address, "Welcome")
def send_reset(self, address, token):
self._smtp.send(address, f"Reset: {token}")
# user_formatting.py — no class, no state
def format_joined_at(user, clock):
return clock.format(user.joined_at)
Each class now has LCOM4 = 1, and the formatter is a one-line function where it belongs.
Options¶
| Option | Default | Description |
|---|---|---|
lcom.low_threshold |
2 |
At or below this, the class is reported as low risk. |
lcom.medium_threshold |
5 |
Above this, the class is high risk. |
References¶
- Hitz, M. & Montazeri, B. Chidamber and Kemerer's Metrics Suite: A Measurement Theory Perspective. IEEE TSE, 1996 (LCOM4 definition).
- Implementation:
internal/analyzer/lcom.go. - Rule catalog · high-class-coupling