When we're developing an object oriented systems in Python, there are often instances where we have to create abstract classes. These classes define a basic structure that we expect other parts of the code to implement. or instance, in the context of databases, we create an abstract class that contains fundamental structure for a database class. The derived classes then need to implement the abstract methods to form a complete structure. The challenge arises when a derived class doesn't follow the expected signature of the abstract class.

Abstract class:

1from abc import ABC, abstractmethod
2
3class Database(ABC):
4 def setup(self):
5 print("setup step")
6
7 @abstractmethod
8 def connect(self) -> bool:
9 pass
10
11 @abstractmethod
12 def query(self, query: str) -> list[dict]:
13 pass

Derived classes:

1from database import Database
2
3class PostgreSQL(Database):
4 def connect(self) -> bool:
5 print("PostgreSQL trying to establish a connection")
6 return True
7
8 def query(self, query: str) -> list[dict]:
9 print("running PostgreSQL query")
10 return [{"data": "something"}]
11
12class MySQL(Database):
13 def connect(self) -> bool:
14 print("MySQL trying to establish a connection")
15 return True
16
17 def query(self, query: str) -> list[dict]:
18 print("running MySQL query")
19 return [{"data": "something else"}]

Test:

1from database import MySQL
2MySQL().connect()
3output = MySQL().query("where 1")
4print(output)

Output:

$ python test.py
MySQL trying to establish a connection
running MySQL query
[{'data': 'something else'}]

In this particular scenario, everything functions smoothly without any issues. However, as a project expands and more individuals become involved, there is a possibility that some might not follow the established standards exactly. Python itself doesn't prevent this.

Consider this implementation of the MySQL class, which completely deviates from the signatures of the abstract class:

1from database import Database
2
12class MySQL(Database):
13 def connect(self):
14 print("MySQL trying to establish a connection")
15
16 def query(self, query, limit):
17 print("running MySQL query")
18 return "something else"

Python test:

1from database import MySQL
2MySQL().connect()
3output = MySQL().query("where 1", 100)
4print(output)

Output:

$ python test.py
MySQL trying to establish a connection
running MySQL query
something else

As you can see, Python didn't check the signature at all.

Allowing this structure to merge into your codebase would violate the established standards.

Now, can we compel everyone to adhere to the abstract class signatures? This is where the abcmeta project comes into play.

$ pip install abcmeta

The only thing needs to be changed in your code, is in your abstract class file:

from:

from abc import ABC, abstractmethod

to:

from abcmeta import ABC, abstractmethod

This library then examines and ensures that all signatures align with the abstract class.

Now, let's run the previous example and see what will happen:

$ python test_db.py
Traceback (most recent call last):
  File "/home/mort/project/test.py", line 1, in <module>
    from database import MySQL
  File "/home/mort/project/database.py", line 13, in <module>
    class MySQL(Database):
  File "<frozen abc>", line 106, in __new__
  File "/home/mort/project/.venv/lib/python3.12/site-packages/abcmeta/__init__.py", line 198, in __init_subclass__
    raise AttributeError("\n{}".format("\n\n".join(errors)))
AttributeError:
1: incorrect signature.
Signature of the derived method is not the same as parent class:
- connect(self) -> bool
?              --------

+ connect(self)
Derived method expected to return in '<class 'bool'>' type, but returns 'typing.Any'

2: incorrect signature.
Signature of the derived method is not the same as parent class:
- query(self, query: str) -> list[dict]
+ query(self, query, limit)
Derived method expected to return in 'list[dict]' type, but returns 'typing.Any'

The error message clarifies that the signature differs from that of the abstract class, and explains with all details.

Additionally, it's important to know that abcmeta utilizes metaclasses implying that it examines the class when the class is defined!

Therefore, if you modify the test code to something like this:

from database import MySQL

Then you'll get the same error result.

So, by using these kinds of libraries in Python, we can proactively avoid future errors by forcing derived classes to follow the abstract class signatures, similar to strong-typed programming languags.