Skip to main content

strict-abc-lsp

CI PyPI version Python versions PyPI - Types 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 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)

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

Built distribution (wheel)

Table of built distributions (wheels) for strict-abc-lsp 0.2.1
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.1 This release

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