A Flake8 plugin that nudges you toward composition over inheritance. It detects when classes inherit from concrete internal classes — cases where composition would reduce coupling — and flags concrete methods in abstract base classes that should remain pure interfaces.
Status: Under development. Not yet published to PyPI.
- Python: 3.11, 3.12, 3.13
- Flake8: 6.x, 7.x
- Operating systems: Linux, macOS, Windows
pip install flake8-inheritance-
Install the plugin (it registers itself with Flake8 automatically):
pip install flake8-inheritance
-
Enable the plugin rules (it is off by default):
flake8 --enable-extensions=INH src/
Or configure this once in
.flake8/setup.cfg:[flake8] enable-extensions = INH
-
Configure
--project-packagesso the plugin knows which imports are part of your project. Add it to.flake8orsetup.cfg:# .flake8 or setup.cfg [flake8] enable-extensions = INH project-packages = myproject
Without this option, same-file inheritance and relative imports (e.g.
from .models import Base) are still flagged. However, absolute imports from your own packages are treated as external and silently allowed. -
Run Flake8:
flake8 src/
flake8 --versionThe output should include a line like:
flake8-inheritance: X.Y.Z
| Code | Description |
|---|---|
| INH001 | Inheritance from an internal concrete class (use composition) |
| INH002 | Concrete method in an abstract base class (ABCs should be pure) |
Triggered when a class inherits from a concrete (non-abstract) class
that lives in the same file or in one of the configured
--project-packages.
class Engine:
def start(self) -> None: ...
# INH001: Inheritance from internal class 'Engine' is not allowed
class TurboEngine(Engine):
def start(self) -> None: ...Fix: replace inheritance with composition:
class TurboEngine:
def __init__(self, engine: Engine) -> None:
self._engine = engine
def start(self) -> None:
self._engine.start()Triggered when a class that derives from abc.ABC (or uses
abc.ABCMeta) defines a non-abstract method.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self) -> str: ...
# INH002: ABC 'Animal' contains concrete method 'legs'
def legs(self) -> int:
return 4Fix: make the method abstract, or move the default implementation into a concrete class.
This section documents every plugin option and the most common Flake8-native
settings used to adopt flake8-inheritance in real projects.
| Option | Default | Meaning |
|---|---|---|
--enable-extensions |
unset | Plugin checks are disabled until INH is enabled |
--project-packages |
empty (unset) | Absolute imports are external unless package is configured |
--inh002-allowed-dunders |
unset (None) |
All dunder methods are allowed in ABCs |
Comma-separated list of top-level package names that belong to your project. The plugin uses this to distinguish your code from third-party libraries.
[flake8]
enable-extensions = INH
project-packages = myproject,myproject_utilsDefault: empty (unset)
When left unset (or set to an explicit empty value), the plugin still flags:
- inheritance from classes defined in the same file
- inheritance via relative imports (for example,
from .base import Base)
But it treats absolute imports as external unless their top-level package is listed here.
Comma-separated list of dunder method names that are permitted as
concrete methods in ABCs (e.g., __init__, __repr__).
[flake8]
enable-extensions = INH
inh002-allowed-dunders = __init__,__repr__Default: unset (None), which allows all dunder methods.
This option has three useful states:
- Unset (no config value): all dunder methods are allowed.
- Empty string (
inh002-allowed-dunders =): no dunder methods are allowed. - Non-empty list: only listed dunder methods are allowed.
You can configure these options in whichever Flake8 config style your project uses.
[flake8]
enable-extensions = INH
project-packages = myproject,myproject_utils
inh002-allowed-dunders = __init__,__repr__
# Flake8-native filtering controls
select = INH,E,F,W
extend-ignore = E203
per-file-ignores =
tests/*:INH001[flake8]
enable-extensions = INH
project-packages = myproject,myproject_utils
inh002-allowed-dunders = __init__,__repr__
# Flake8-native filtering controls
select = INH,B,C,E,F,W
extend-ignore = E203,W503
per-file-ignores =
tests/*:INH001
src/myproject/legacy/*.py:INH001,INH002Flake8 does not read pyproject.toml natively. Use
flake8-pyproject to enable
this format.
[tool.flake8]
enable-extensions = ["INH"]
project-packages = ["myproject", "myproject_utils"]
inh002-allowed-dunders = ["__init__", "__repr__"]
# Flake8-native filtering controls
select = ["INH", "E", "F", "W"]
extend-ignore = ["E203", "W503"]
per-file-ignores = [
"tests/*:INH001",
"src/myproject/legacy/*.py:INH001,INH002",
]These are provided by Flake8 itself (not this plugin), but they are important for adoption:
--select: run only selected code families (for example,--select=INHto focus exclusively on inheritance rules).--extend-ignore: suppress specific codes while keeping defaults.--per-file-ignores: carve out exceptions for known legacy paths.
For existing codebases, adopt incrementally:
- Start in report-only mode by running Flake8 with
--select=INHin CI, but do not fail the build yet. - Track and review findings; classify true positives vs intentional patterns.
- Add temporary
per-file-ignoresfor legacy modules to keep signal high. - Fix violations in new/changed code first.
- Remove ignores over time and then enforce INH rules as required checks.
Example report-only CI command:
flake8 --select=INH src tests || trueExample enforced command once ready:
flake8 --select=INH src tests- Dynamic base classes are skipped: if a base class is constructed dynamically (for example, through metaprogramming or runtime factory calls), the plugin cannot resolve it statically and does not emit INH001 for that base. Workaround: favor explicit, statically imported base classes where you want enforcement.
- Import aliases keep only top-level package classification: aliases such as
from pkg.module import Base as RenamedBaseare tracked underRenamedBase, but classification still uses only the top-level package (pkg), not the full defining module path (pkg.module). - Star imports are skipped for unresolved names: with
from pkg.module import *, names introduced indirectly cannot be reliably mapped to import paths, so inheritance checks for those names are skipped. Workaround: replace star imports with explicit imports. - Re-exports through
__init__.pyare classified by top-level package name: imports likefrom pkg import Xare classified aspkgwithout tracing to a defining submodule, so re-exports are not distinguished from direct exports. Workaround: import directly from the defining module when that distinction matters. - Analysis is single-file only: decisions are made from one file's AST and
import statements without cross-file type inference.
Workaround: configure
project-packagesand use explicit imports to improve internal/external classification accuracy within this single-file model.
BSD-3-Clause