strict-abc-lsp
strict-abc-lsp is a Python library that adds runtime signature validation for abstract base classes.
It provides StrictABCMeta and StrictABC, which check concrete implementations of abstract methods at class definition time and fail fast when a subclass violates LSP-friendly signature rules.
Distribution name:
strict-abc-lsp
Import name:strict_abc
Why?
Python's standard abc module checks whether an abstract method is implemented, but it does not check whether the implementation preserves the original calling contract.
For example, this is allowed by standard abc:
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def process(self, data: dict, cache: bool = True) -> str:
...
class Impl(Base):
def process(self, data: dict, cache: bool) -> str:
return "ok"
The method is implemented, so Python considers the class concrete.
However, the parent contract allowed this call:
obj.process(data)
The implementation above breaks it, because cache is no longer optional.
This is a violation of the Liskov Substitution Principle.
strict-abc-lsp detects such violations early, at class definition time.
Installation
pip install strict-abc-lsp
The package is typed and ships with py.typed.
import strict_abc
Quickstart
from abc import abstractmethod
from strict_abc import StrictABC
class BaseService(StrictABC):
@abstractmethod
def process(self, data: dict, cache: bool = True) -> str:
...
class ValidService(BaseService):
def process(self, data: dict, cache: bool = True) -> str:
return "ok"
This works.
But this raises TypeError at class definition time:
class InvalidService(BaseService):
def process(self, data: dict, cache: bool) -> str:
return "ok"
Error:
TypeError: InvalidService.process: removing default value for 'cache'
strengthens precondition and violates LSP
What it checks
strict-abc-lsp validates many common LSP-violating signature changes.
1. Removing default values
Not allowed:
class Base(StrictABC):
@abstractmethod
def m(self, a: int = 1) -> None:
...
class Impl(Base):
def m(self, a: int) -> None:
...
Allowed:
class Base(StrictABC):
@abstractmethod
def m(self, a: int) -> None:
...
class Impl(Base):
def m(self, a: int = 1) -> None:
...
Adding defaults weakens the precondition and is safe.
2. Adding new required parameters
Not allowed:
class Base(StrictABC):
@abstractmethod
def m(self, a: int) -> None:
...
class Impl(Base):
def m(self, a: int, b: int) -> None:
...
Allowed if the new parameter is optional:
class Impl(Base):
def m(self, a: int, b: int = 0) -> None:
...
3. Removing *args or **kwargs
Not allowed:
class Base(StrictABC):
@abstractmethod
def m(self, *args: int) -> None:
...
class Impl(Base):
def m(self) -> None:
...
Also not allowed:
class Base(StrictABC):
@abstractmethod
def m(self, **kwargs: int) -> None:
...
class Impl(Base):
def m(self) -> None:
...
Adding *args or **kwargs is allowed because it expands the accepted call surface.
4. Keyword compatibility
Keyword parameter names are part of the public calling contract.
Not allowed:
class Base(StrictABC):
@abstractmethod
def connect(self, *, host: str) -> None:
...
class Impl(Base):
def connect(self, *, address: str) -> None:
...
Parent callers may use:
client.connect(host="localhost")
The implementation above breaks that call.
5. Descriptor compatibility
The implementation must preserve the descriptor type of the abstract method.
For example, a static method must remain a static method:
class Base(StrictABC):
@staticmethod
@abstractmethod
def parse(raw: str) -> dict:
...
class ValidImpl(Base):
@staticmethod
def parse(raw: str) -> dict:
return {}
This raises TypeError:
class InvalidImpl(Base):
def parse(self, raw: str) -> dict:
return {}
strict-abc-lsp understands:
- instance methods;
staticmethod;classmethod;property.
6. Return type covariance
When enabled, return type annotations are checked for basic covariance.
Allowed:
class Base(StrictABC):
__strict_options__ = {"check_return_type": True}
@abstractmethod
def get(self) -> object:
...
class Impl(Base):
def get(self) -> str:
return "ok"
Not allowed:
class Base(StrictABC):
__strict_options__ = {"check_return_type": True}
@abstractmethod
def get(self) -> str:
...
class Impl(Base):
def get(self) -> object:
return object()
Options
Validation behavior can be configured using __strict_options__.
class Base(StrictABC):
__strict_options__ = {
"check_names": True,
"check_defaults": True,
"check_types": True,
"check_return_type": True,
}
@abstractmethod
def fetch(self, url: str) -> bytes:
...
Available options
| Option | Default | Description |
|---|---|---|
check_names |
False |
Require exact parameter name matching. |
check_defaults |
True |
Forbid removing default values. |
check_types |
False |
Require exact parameter type annotation matching. |
check_return_type |
False |
Require covariant return type annotations. |
Default configuration:
{
"check_names": False,
"check_defaults": True,
"check_types": False,
"check_return_type": False,
}
Option inheritance
Options are merged through the MRO.
Example:
class Base(StrictABC):
__strict_options__ = {"check_types": True}
@abstractmethod
def m(self, a: int) -> None:
...
class Child(Base):
__strict_options__ = {"check_names": True}
def m(self, a: int) -> None:
...
In this case, both options are active:
{
"check_types": True,
"check_names": True,
}
Using the metaclass directly
You can use StrictABCMeta without inheriting from StrictABC:
from abc import abstractmethod
from strict_abc import StrictABCMeta
class Base(metaclass=StrictABCMeta):
@abstractmethod
def run(self, timeout: int = 30) -> None:
...
class Impl(Base):
def run(self, timeout: int = 30) -> None:
...
Relation to SOLID
This library is primarily focused on the Liskov Substitution Principle.
The Liskov Substitution Principle says that objects of a subtype should be usable in place of objects of the parent type without breaking the program.
strict-abc-lsp enforces part of this principle at the signature level:
- implementations must not strengthen preconditions;
- implementations must not remove accepted calling forms;
- implementations must not remove variadic parameters declared by the parent;
- implementations should preserve keyword-call compatibility;
- return types should remain covariant when checking is enabled.
However, this library does not verify full behavioral substitution.
It does not check:
- business rules;
- invariants;
- side effects;
- exception guarantees;
- method semantics;
- thread-safety;
- performance contracts.
For behavioral contracts, consider tools such as icontract or design-by-contract approaches.
How it differs from other tools
Compared to abc
Standard abc:
Checks whether abstract methods are implemented.
strict-abc-lsp:
Also checks whether implementations preserve the abstract calling contract.
Compared to mypy / pyright
Static type checkers can detect many incompatible overrides.
But they:
- require typed code;
- are not always used by plugin authors;
- do not protect runtime-generated classes;
- do not protect dynamically loaded extensions;
- may not be part of every deployment pipeline.
strict-abc-lsp provides an additional runtime safety layer.
It is not a replacement for static typing.
It is a complement to it.
When to use
This library is especially useful for:
- plugin systems;
- frameworks;
- SDKs;
- public base classes;
- extension points;
- educational projects about LSP and OOP;
- projects where runtime safety is important;
- codebases where not all contributors use strict static typing.
Limitations
strict-abc-lsp is a practical runtime checker, not a complete formal LSP verifier.
Current limitations include:
- parameter type checking is exact, not fully contravariant;
- generic return type covariance is handled conservatively;
- overloaded functions are not fully analyzed;
- custom descriptors may not be fully supported;
- behavioral semantics are not checked;
- complex signature reordering may still produce edge cases.
Requirements
- Python 3.10+
Development
The project uses:
Install dependencies:
make install
Run lint, type check, tests, build and dist check:
make all
Run tests with coverage:
make cov
Install pre-commit hooks:
make pre-commit-install
Project layout
strict-abc/
├── src/
│ └── strict_abc/
│ ├── __init__.py
│ ├── _meta.py
│ └── py.typed
├── tests/
│ └── test_strict_abc.py
├── .github/
│ └── workflows/
│ └── ci.yml
├── pyproject.toml
├── Makefile
├── README.md
└── LICENSE
Publishing
This project is published as:
strict-abc-lsp
Install it with:
pip install strict-abc-lsp
Import it with:
import strict_abc
Versioning
This project follows semantic versioning:
MAJOR.MINOR.PATCH
PATCH— backward-compatible bug fixes;MINOR— backward-compatible new features;MAJOR— incompatible API changes.
License
MIT
Metadata
Release files for strict-abc-lsp 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| strict_abc_lsp-0.2.1.tar.gz | 12.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| strict_abc_lsp-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.3 kB
Release files / strict_abc_lsp-0.2.1.tar.gz
| Download URL | strict_abc_lsp-0.2.1.tar.gz |
|---|---|
| Size | 12.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e2e5483c3c1648b4bb4bf3b36b0c3c9c6ad7b33277146cf8f102a9460906c431
|
|
BLAKE2b-256 checksum How to use checksums |
d81e2473dec6cd0b5efbb5c78a2845a9b8c2d54beb9cbdca76accfd76798e3e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.3.2 CPython/3.14.2 Darwin/25.5.0
|
Release files / strict_abc_lsp-0.2.1-py3-none-any.whl
| Download URL | strict_abc_lsp-0.2.1-py3-none-any.whl |
|---|---|
| Size | 10.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e9cbf60a2fd533713e1d3c1f9a35bba6a1efa5bd221e43727f98e1cc195194f3
|
|
BLAKE2b-256 checksum How to use checksums |
42a34f0c9855c4a41b90a18f8b2279bbd946784d7147e4cbc7fb8a8c86ba6277
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.3.2 CPython/3.14.2 Darwin/25.5.0
|