Skip to main content

strict-abc-lsp

CI codecov PyPI version Python versions PyPI - Types PyPI Downloads License: MIT Ruff mypy

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 (specifically LSPViolation) 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()

7. Async/sync compatibility

An abstract async def method must be implemented as an async def method, and a regular def must remain synchronous. Mixing them raises an error.


Options

Validation behavior can be configured using __strict_options__.

class Base(StrictABC):
    __strict_options__ = {
        "check_names": True,
        "check_defaults": True,
        "check_types": True,
        "check_types_contravariant": True,
        "check_return_type": True,
        "mode": "error",
    }

    @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_types_contravariant False Require contravariant parameter types (child may widen the accepted type). Takes precedence over check_types.
check_return_type False Require covariant return type annotations.
mode "error" "error" raises LSPViolation (a TypeError), "warn" emits a UserWarning.

Default configuration:

{
    "check_names": False,
    "check_defaults": True,
    "check_types": False,
    "check_types_contravariant": False,
    "check_return_type": False,
    "mode": "error",
}

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: ...

Using the @strict decorator

If you already have a base class or metaclass hierarchy and cannot inherit from StrictABC, you can use the @strict class decorator to inject options:

from abc import ABC, abstractmethod
from strict_abc import StrictABCMeta, strict

@strict(check_types=True, check_return_type=True)
class Base(ABC, metaclass=StrictABCMeta):
    @abstractmethod
    def run(self) -> None: ...

Exempting methods

If you intentionally want to narrow the calling contract and acknowledge the LSP violation, you can mark a method with @lsp_exempt:

from strict_abc import StrictABC, lsp_exempt
from abc import abstractmethod

class Base(StrictABC):
    @abstractmethod
    def process(self, data: dict, cache: bool = True) -> str: ...

class Impl(Base):
    @lsp_exempt
    def process(self, data: dict) -> str:
        return "ok"

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;
  • parameter types may widen (contravariance) 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 contravariance is handled conservatively (e.g., basic subclasses and unions are supported, but complex generic aliases may not be fully resolved);
  • 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.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for strict-abc-lsp 0.3.0
File Size Uploaded
strict_abc_lsp-0.3.0.tar.gz 17.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for strict-abc-lsp 0.3.0
File Interpreter ABI Platform
strict_abc_lsp-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.2 kB

Release files / strict_abc_lsp-0.3.0.tar.gz

Download URL strict_abc_lsp-0.3.0.tar.gz
Size 17.9 kB
Tags Source
SHA-256 checksum
How to use checksums
014aa14ca2140248cef56432ec0871f3bb6dc25ae5541f2b77245adb02bb421b
BLAKE2b-256 checksum
How to use checksums
025b0b9f970475306c6e0b8568ba714024cd8a5b57a574e729992ad68fb07679
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.3.2 CPython/3.14.2 Darwin/25.6.0

Release files / strict_abc_lsp-0.3.0-py3-none-any.whl

Download URL strict_abc_lsp-0.3.0-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
164fdd091d28931bd2fe7e22c6b6aac6d3fb888421dc6b66dc3fc32b7ce4608d
BLAKE2b-256 checksum
How to use checksums
b4aeb657871f8a838939a5043f9ba5f7e90bfc207c8f44f1aefe4a74d40ef355
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.3.2 CPython/3.14.2 Darwin/25.6.0

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page