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:
1 from abc import ABC, abstractmethod
2
3 class 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:
1 from database import Database
2
3 class 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
12 class 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:
1 from database import MySQL
2 MySQL().connect()
3 output = MySQL().query("where 1")
4 print(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:
1 from database import Database
2
12 class 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:
1 from database import MySQL
2 MySQL().connect()
3 output = MySQL().query("where 1", 100)
4 print(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.