A static analysis tool for Python OOP best practices
Project description
OOP Analyzer
A static analysis tool for Python that checks adherence to Object-Oriented Programming best practices. Safe by design - analyzes code using AST parsing only, never executes any code.
Features
- Encapsulation Rule: Detects direct property access violations (Tell Don't Ask principle)
- Coupling Rule: Measures coupling, builds dependency graphs, differentiates stdlib (soft warning) vs external dependencies (warning)
- Null Object Rule: Finds None usage and Optional type hints that could introduce nulls
- Polymorphism Rule: Detects if/elif chains replaceable by polymorphism
- Functions to Objects Rule: Identifies functions that could be better represented as objects
- Type Code Rule: Detects conditionals checking constants/enums that should use State/Strategy pattern
- Reference Exposure Rule: Finds methods returning internal mutable state that breaks encapsulation
- Dictionary Usage Rule: Detects dictionaries that should be dataclasses/Pydantic models (allows API boundaries)
- Boolean Flag Rule: Detects boolean flag parameters causing behavior branching
Installation
From PyPI (recommended)
pip install oop-analyzer
From source
# Clone the repository
git clone https://github.com/agustindorda/oop-analyzer.git
cd oop-analyzer
# Install with pip
pip install .
# Or install in development mode with dev dependencies
pip install -e ".[dev]"
Using uv
uv add oop-analyzer
Usage
Command Line
# Analyze a single file
oop-analyzer path/to/file.py
# Analyze a directory
oop-analyzer path/to/project/
# Analyze a module
oop-analyzer path/to/module/
# Specify output format (json, xml, html)
oop-analyzer path/to/file.py -f html -o report.html
# Enable only specific rules
oop-analyzer path/to/file.py --rules encapsulation coupling
# Disable specific rules
oop-analyzer path/to/file.py --disable-rules functions_to_objects
# List available rules
oop-analyzer --list-rules
# Generate default config file
oop-analyzer --init-config oop-analyzer.json
Python API
from oop_analyzer import OOPAnalyzer, AnalyzerConfig
# Default configuration (all rules enabled)
analyzer = OOPAnalyzer()
# Analyze source code
report = analyzer.analyze_source('''
def process(user):
print(user.name) # Encapsulation violation
''')
# Analyze a file
report = analyzer.analyze_file("path/to/file.py")
# Analyze a directory or module
report = analyzer.analyze("path/to/project/")
# Get formatted output
json_output = analyzer.format_report(report, "json")
html_output = analyzer.format_report(report, "html")
xml_output = analyzer.format_report(report, "xml")
# Custom configuration
config = AnalyzerConfig()
config.enable_only("encapsulation", "null_object")
config.output_format = "html"
analyzer = OOPAnalyzer(config)
Configuration
Create a oop-analyzer.json file:
{
"rules": {
"encapsulation": {
"enabled": true,
"severity": "warning",
"options": {
"allow_self_access": true,
"max_chain_length": 1
}
},
"coupling": {
"enabled": true,
"options": {
"max_imports_warning": 10
}
},
"null_object": true,
"polymorphism": {
"enabled": true,
"options": {
"min_branches": 3
}
},
"functions_to_objects": {
"enabled": true,
"options": {
"max_params": 4
}
}
},
"output_format": "json",
"exclude_patterns": ["**/test_*.py", "**/*_test.py", "**/tests/**"]
}
Rules
Encapsulation (Tell Don't Ask)
Detects direct property access on objects. In OOP, we should "tell" objects what to do, not "ask" them for data.
Bad:
if user.age > 18:
print(user.name)
Good:
if user.is_adult():
user.greet()
Better:
adult_user.greet()
Coupling
Measures module coupling through import analysis. Shows dependency graphs and identifies highly-coupled modules where abstractions might be missing.
Null Object
Detects None usage patterns that could be replaced by the Null Object pattern:
if x is Nonechecksreturn Nonestatements- Parameters with
Nonedefaults
Polymorphism
Finds if/elif chains and type checks that could be replaced by polymorphism:
- Long if/elif chains checking the same variable
isinstance()checks- Type/kind attribute comparisons
Functions to Objects
Identifies functions that might be better as objects:
- Functions with many parameters
- Functions returning dictionaries
- Groups of related functions with common prefixes
Type Code (NEW)
Detects type code conditionals that should be replaced with polymorphism:
Bad:
class Bird:
def getSpeed(self):
if self.type == EUROPEAN:
return self.getBaseSpeed()
elif self.type == AFRICAN:
return self.getBaseSpeed() - self.getLoadFactor()
elif self.type == NORWEGIAN_BLUE:
return 0 if self.isNailed else self.getBaseSpeed(self.voltage)
Good: Use State/Strategy pattern or subclasses:
class Bird(ABC):
@abstractmethod
def getSpeed(self) -> float: pass
class EuropeanBird(Bird):
def getSpeed(self) -> float:
return self.getBaseSpeed()
class AfricanBird(Bird):
def getSpeed(self) -> float:
return self.getBaseSpeed() - self.getLoadFactor()
References:
- https://refactoring.guru/replace-type-code-with-state-strategy
- https://refactoring.guru/replace-type-code-with-subclasses
Reference Exposure (NEW)
Detects methods that return references to internal mutable state, breaking encapsulation:
Bad:
class Container:
def __init__(self):
self._items = []
def get_items(self):
return self._items # External code can modify internal state!
Good: Return a copy or immutable view:
class Container:
def __init__(self):
self._items = []
def get_items(self):
return list(self._items) # Return a copy
# Or return a tuple for immutability
def get_items_readonly(self):
return tuple(self._items)
Dictionary Usage (NEW)
Detects dictionary usage that should be replaced by proper objects (dataclasses, Pydantic models, etc.). Dictionaries are acceptable at API boundaries (parsing REST responses), but abstraction layers should use typed objects.
Bad:
def get_user():
return {"name": "John", "age": 30, "email": "john@example.com"}
def process(user: dict):
print(user["name"]) # No type safety, easy to typo keys
Good: Use dataclasses or Pydantic models:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str
def get_user() -> User:
return User(name="John", age=30, email="john@example.com")
def process(user: User):
print(user.name) # Type-safe, IDE autocomplete
Acceptable (API boundary):
def parse_api_response(response: dict) -> User:
# Converting from dict at the boundary is fine
return User(**response)
Extending with New Rules
Create a new rule by inheriting from BaseRule:
from oop_analyzer.rules.base import BaseRule, RuleResult, RuleViolation
class MyCustomRule(BaseRule):
name = "my_rule"
description = "My custom OOP rule"
def analyze(self, tree, source, file_path):
violations = []
# Analyze the AST tree
# Add violations as needed
return RuleResult(
rule_name=self.name,
violations=violations,
)
Register in oop_analyzer/rules/__init__.py:
RULE_REGISTRY["my_rule"] = MyCustomRule
Safety
The analyzer is designed to be safe:
- No code execution: Only AST parsing, never
exec()oreval() - File validation: Checks file existence, type, and size limits
- Syntax validation: Gracefully handles malformed code
Running Tests
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
pytest
# Run with coverage
pytest --cov=oop_analyzer
# Run specific test file
pytest tests/test_rules/test_encapsulation.py
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_oop_analyzer-0.1.0.tar.gz.
File metadata
- Download URL: python_oop_analyzer-0.1.0.tar.gz
- Upload date:
- Size: 112.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5786e594b6fd27cbc99fd66ddb328b18b4287387d6330f45a640768b4e49941e
|
|
| MD5 |
7b76f10cd5b477775e1cf1b0ffef19ce
|
|
| BLAKE2b-256 |
85446c18c9861223160cd10309bc7f70664320b9f44a933f50e3df5765140974
|
Provenance
The following attestation bundles were made for python_oop_analyzer-0.1.0.tar.gz:
Publisher:
publish.yml on angdmz/oop-analyzer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_oop_analyzer-0.1.0.tar.gz -
Subject digest:
5786e594b6fd27cbc99fd66ddb328b18b4287387d6330f45a640768b4e49941e - Sigstore transparency entry: 929106073
- Sigstore integration time:
-
Permalink:
angdmz/oop-analyzer@d0945bafea32ac94650eb74fbf69ce670c7a40e1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/angdmz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d0945bafea32ac94650eb74fbf69ce670c7a40e1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file python_oop_analyzer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: python_oop_analyzer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 53.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1af6e0e3f0c84e0d2538e5d88e878b45f145c64d4bd68441114d81a7967f25a1
|
|
| MD5 |
ebf23d2d76f921c9b2bcf145d7f27a5c
|
|
| BLAKE2b-256 |
e5b72dea84041c65667e1342f1a403c4eef8abeb8cd0c3c259b1353d2ff930f0
|
Provenance
The following attestation bundles were made for python_oop_analyzer-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on angdmz/oop-analyzer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_oop_analyzer-0.1.0-py3-none-any.whl -
Subject digest:
1af6e0e3f0c84e0d2538e5d88e878b45f145c64d4bd68441114d81a7967f25a1 - Sigstore transparency entry: 929106075
- Sigstore integration time:
-
Permalink:
angdmz/oop-analyzer@d0945bafea32ac94650eb74fbf69ce670c7a40e1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/angdmz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d0945bafea32ac94650eb74fbf69ce670c7a40e1 -
Trigger Event:
release
-
Statement type: