6.1. Abstract Class

  • Since Python 3.0: PEP 3119 -- Introducing Abstract Base Classes

  • Cannot instantiate

  • Possible to indicate which method must be implemented by child

  • Inheriting class must implement all methods

  • Some methods can have implementation

  • Python Abstract Base Classes [1]

abstract class

Class which can only be inherited, not instantiated. Abstract classes can have regular methods which will be inherited normally. Some methods can be marked as abstract, and those has to be overwritten in subclasses.

abstract method

Method which has to be present (implemented) in a subclass.

abstract static method

Static method which must be implemented in a subclass.

abstract property

Class variable which has to be present in a subclass.

6.1.1. SetUp

>>> from abc import ABC, ABCMeta, abstractmethod, abstractproperty

6.1.2. Syntax

  • Inherit from ABC

  • At least one method must be abstractmethod or abstractproperty

  • Body of the method is not important, it could be raise NotImplementedError or pass

>>> class Account(ABC):
...     @abstractmethod
...     def login(self) -> None:
...         raise NotImplementedError

You cannot create instance of a class Account as of this is the abstract class:

>>> mark = Account()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Account without an implementation for abstract method 'login'

6.1.3. Implement Abstract Methods

  • All abstract methods must be covered

  • Abstract base class can have regular (not abstract) methods

  • Regular methods will be inherited as normal

  • Regular methods does not need to be overwritten

Abstract base class:

>>> class Account(ABC):
...     def __init__(self, username: str, password: str) -> None:
...         self.username = username
...         self.password = password
...
...     @abstractmethod
...     def login(self) -> None:
...         raise NotImplementedError
...
...     @abstractmethod
...     def logout(self) -> None:
...         raise NotImplementedError

Implementation:

>>> class User(Account):
...     def login(self) -> None:
...         print('Logging-in')
...
...     def logout(self) -> None:
...         print('Logging-out')

Use:

>>> mark = User(username='mwatney', password='Ares3')
>>>
>>> mark.login()
Logging-in
>>>
>>> mark.logout()
Logging-out

Mind, that all abstract methods must be covered, otherwise it will raise an error. Regular methods (non-abstract) will be inherited as normal and they does not need to be overwritten in an implementing class.

6.1.4. ABCMeta

  • Uses metaclass=ABCMeta

  • Not recommended since Python 3.4

  • Use inheriting ABC instead

There is also an alternative (older) way of defining abstract base classes. It uses metaclass=ABCMeta specification during class creation. This method is not recommended since Python 3.4 when ABC class was introduce to simplify the process.

>>> class Account(metaclass=ABCMeta):
...     def __init__(self, username: str, password: str) -> None:
...         self.username = username
...         self.password = password
...
...     @abstractmethod
...     def login(self) -> None:
...         raise NotImplementedError

6.1.5. Abstract Property

  • abc.abstractproperty is deprecated since Python 3.3

  • Use property with abc.abstractmethod instead

>>> class Account(ABC):
...     @abstractproperty
...     def AGE_MIN(self) -> int:
...         raise NotImplementedError
...
...     @abstractproperty
...     def AGE_MAX(self) -> int:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     AGE_MIN: int = 18
...     AGE_MAX: int = 65

Since 3.3 instead of @abstractproperty using both @property and @abstractmethod is recommended.

>>> class Account(ABC):
...     @property
...     @abstractmethod
...     def AGE_MIN(self) -> int:
...         raise NotImplementedError
...
...     @property
...     @abstractmethod
...     def AGE_MAX(self) -> int:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     AGE_MIN: int = 18
...     AGE_MAX: int = 65

Mind that the order here is important and it cannot be the other way around. Decorator closest to the method must be @abstractmethod and then @property at the most outer level. This is because @abstractmethod sets special attribute on the method and then this method with attribute is turned to the property. This does not work if you reverse the order.

6.1.6. Problem: Base Class Has No Abstract Method

In order to use Abstract Base Class you must create at least one abstract method. Otherwise it won't prevent from instantiating:

>>> class Account(ABC):
...     pass
>>>
>>>
>>> mark = Account()
>>> mark  
<__main__.Account object at 0x...>

The code above will allo to create mark from Account because this class did not have any abstract methods.

6.1.7. Problem: Base Class Does Not Inherit From ABC

In order to use Abstract Base Class you must inherit from ABC in your base class. Otherwise it won't prevent from instantiating:

>>> class Account:
...     @abstractmethod
...     def login(self) -> None:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     pass
>>>
>>>
>>> mark = User()
>>> mark  
<__main__.User object at 0x...>

This code above will allow to create mark from User because Account class does not inherit from ABC.

6.1.8. Problem: All Abstract Methods are not Implemented

Must implement all abstract methods:

>>> class Account(ABC):
...     @abstractmethod
...     def login(self) -> None:
...         raise NotImplementedError
...
...     @abstractmethod
...     def logout(self) -> None:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     pass
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for abstract methods 'login', 'logout'

The code above will prevent from creating User instance, because class User does not overwrite all abstract methods. In fact it does not overwrite any abstract method at all.

6.1.9. Problem: Some Abstract Methods are not Implemented

All abstract methods must be implemented in child class:

>>> class Account(ABC):
...     @abstractmethod
...     def login(self) -> None:
...         raise NotImplementedError
...
...     @abstractmethod
...     def logout(self) -> None:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     def login(self) -> None:
...         print('Logging-in')
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for abstract method 'logout'

The code above will prevent from creating User instance, because class User does not overwrite all abstract methods. The .login() method is not overwritten. In order abstract class to work, all methods must be covered.

6.1.10. Problem: Child Class has no Abstract Property

  • Using abstractproperty

>>> class Account(ABC):
...     @abstractproperty
...     def AGE_MIN(self) -> int:
...         raise NotImplementedError
...
...     @abstractproperty
...     def AGE_MAX(self) -> int:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     AGE_MIN: int = 18
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for abstract method 'AGE_MAX'

The code above will prevent from creating User instance, because class User does not overwrite all abstract properties. The AGE_MAX is not covered.

6.1.11. Problem: Child Class has no Abstract Properties

  • Using property and abstractmethod

>>> class Account(ABC):
...     @property
...     @abstractmethod
...     def AGE_MIN(self) -> int:
...         raise NotImplementedError
...
...     @property
...     @abstractmethod
...     def AGE_MAX(self) -> int:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     AGE_MIN: int = 18
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for abstract method 'AGE_MAX'

The code above will prevent from creating User instance, because class User does not overwrite all abstract properties. The AGE_MAX is not covered.

6.1.12. Problem: Invalid Order of Decorators

  • Invalid order of decorators: @property and @abstractmethod

  • Should be: first @property then @abstractmethod

>>> class Account(ABC):
...     @abstractmethod
...     @property
...     def AGE_MIN(self) -> int:
...         raise NotImplementedError
...
...     @abstractmethod
...     @property
...     def AGE_MAX(self) -> int:
...         raise NotImplementedError
...
Traceback (most recent call last):
AttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable

Note, that this will not even allow to define User class at all.

6.1.13. Problem: Overwrite ABC File

abc is common name and it is very easy to create file, variable or module with the same name as the library, hence overwriting it. In case of error check all entries in sys.path or sys.modules['abc'] to find what is overwriting it:

>>> from pprint import pprint
>>> import sys
>>> sys.modules['abc']  
<module 'abc' (frozen)>
>>> pprint(sys.path)  
['/Users/watney/myproject',
 '/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pydev',
 '/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pycharm_display',
 '/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/third_party/thriftpy',
 '/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pydev',
 '/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pycharm_matplotlib_backend',
 '/usr/local/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python311.zip',
 '/usr/local/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11',
 '/usr/local/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload',
 '/Users/watney/myproject/venv-3.11/lib/python3.11/site-packages']

6.1.14. Use Case - 0x01

Abstract Class:

>>> from abc import ABC, abstractmethod
>>> class Document(ABC):
...     def __init__(self, filename):
...         self.filename = filename
...
...     @abstractmethod
...     def display(self):
...         pass
>>>
>>>
>>> class PDFDocument(Document):
...     def display(self):
...         print('Display file content as PDF Document')
>>>
>>> class WordDocument(Document):
...     def display(self):
...         print('Display file content as Word Document')
>>> file = PDFDocument('myfile.pdf')
>>> file.display()
Display file content as PDF Document
>>> file = WordDocument('myfile.pdf')
>>> file.display()
Display file content as Word Document
>>> file = Document('myfile.txt')
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Document without an implementation for abstract method 'display'

6.1.15. Use Case - 0x02

>>> from abc import ABC, abstractmethod
>>> class Element(ABC):
...     def __init__(self, name):
...         self.name = name
...
...     @abstractmethod
...     def render(self):
...         pass
>>>
>>>
>>> def render(component: list[Element]):
...     for element in component:
...         element.render()
>>> class TextInput(Element):
...     def render(self):
...         print(f'Rendering {self.name} TextInput')
>>>
>>>
>>> class Button(Element):
...     def render(self):
...         print(f'Rendering {self.name} Button')
>>> login_window = [
...     TextInput(name='Username'),
...     TextInput(name='Password'),
...     Button(name='Submit'),
... ]
>>>
>>> render(login_window)
Rendering Username TextInput
Rendering Password TextInput
Rendering Submit Button

6.1.16. Use Case - 0x03

>>> from abc import ABC, abstractmethod, abstractproperty
>>>
>>> class Account(ABC):
...     age: int
...
...     @property
...     @abstractmethod
...     def AGE_MAX(self) -> int: ...
...
...     @abstractproperty
...     def AGE_MIN(self) -> int: ...
...
...     def __init__(self, age):
...         if not self.AGE_MIN <= age < self.AGE_MAX:
...             raise ValueError('Age is out of bounds')
...         self.age = age
>>> class User(Account):
...     AGE_MIN = 30
...     AGE_MAX = 50
>>> mark = User(age=42)

6.1.17. Use Case - 0x04

>>> from abc import ABC, abstractmethod
>>> from datetime import timedelta
>>>
>>>
>>> class Cache(ABC):
...     @property
...     @abstractmethod
...     def timeout(self) -> timedelta:
...         raise NotImplementedError
...
...     @abstractmethod
...     def set(self, key: str, value: str) -> None:
...         raise NotImplementedError
...
...     @abstractmethod
...     def get(self, key: str) -> str:
...         raise NotImplementedError
...
...     @abstractmethod
...     def delete(self, key: str) -> None:
...         raise NotImplementedError
>>>
>>>
>>> class DatabaseCache(Cache):
...     timeout = timedelta(minutes=10)
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class FilesystemCache(Cache):
...     timeout = timedelta(minutes=10)
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class LocmemCache(Cache):
...     timeout = timedelta(minutes=10)
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>>
>>> cache: Cache = LocmemCache()
>>> cache.set('firstname', 'Mark')
>>> cache.set('lastname', 'Watney')
>>> cache.get('firstname')
>>> cache.get('lastname')
>>> cache.delete('firstname')
>>> cache.delete('lastname')

6.1.18. Further Reading

6.1.19. References

6.1.20. Assignments

Code 6.46. Solution
"""
* Assignment: OOP AbstractClass Syntax
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min

English:
    1. Create abstract class `Account`
    2. Define abstract methods `login()` and `logout()`
    3. Run doctests - all must succeed

Polish:
    1. Stwórz klasę abstrakcyjną `Account`
    2. Zdefiniuj metody abstrakcyjne `login()` i `logout()`
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass, isabstract, ismethod

    >>> assert isclass(Account)
    >>> assert isabstract(Account)
    >>> assert hasattr(Account, 'login')
    >>> assert hasattr(Account, 'logout')
    >>> assert hasattr(Account.login, '__isabstractmethod__')
    >>> assert hasattr(Account.logout, '__isabstractmethod__')
    >>> assert Account.login.__isabstractmethod__ == True
    >>> assert Account.logout.__isabstractmethod__ == True

    >>> result = Account()
    Traceback (most recent call last):
    TypeError: Can't instantiate abstract class Account without an implementation for abstract methods 'login', 'logout'
"""

# Create abstract class `Account`
# Define abstract methods `login()` and `logout()`
class Account:
    pass


Code 6.47. Solution
"""
* Assignment: OOP AbstractClass Implementation
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min

English:
    1. Define class `User` implementing `Account`
    2. Overwrite all abstract methods
    3. Leave `pass` as content
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `User` implementującą `Account`
    2. Nadpisz wszystkie metody abstrakcyjne
    3. Pozostaw `pass` jako treść
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass, isabstract, ismethod

    >>> assert isclass(Account)
    >>> assert isclass(User)
    >>> assert isabstract(Account)
    >>> assert not isabstract(User)
    >>> assert hasattr(Account, 'login')
    >>> assert hasattr(Account, 'logout')
    >>> assert hasattr(User, 'login')
    >>> assert hasattr(User, 'logout')
    >>> assert not hasattr(User.login, '__isabstractmethod__')
    >>> assert not hasattr(User.logout, '__isabstractmethod__')
    >>> assert hasattr(Account.login, '__isabstractmethod__')
    >>> assert hasattr(Account.logout, '__isabstractmethod__')
    >>> assert Account.login.__isabstractmethod__ == True
    >>> assert Account.logout.__isabstractmethod__ == True

    >>> result = Account()
    Traceback (most recent call last):
    TypeError: Can't instantiate abstract class Account without an implementation for abstract methods 'login', 'logout'
    >>> result = User()
    >>> assert ismethod(result.login)
"""
from abc import ABC, abstractmethod

class Account(ABC):
    @abstractmethod
    def login(self):
        pass

    @abstractmethod
    def logout(self):
        pass


# Define class `User` implementing `Account`
# Overwrite all abstract methods
# Leave `pass` as content
class User:
    ...


Code 6.48. Solution
"""
* Assignment: OOP AbstractClass Typing
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min

English:
    1. Modify abstract class `Account`
    2. Add type annotations to all methods
    3. Run doctests - all must succeed

Polish:
    1. Zmodyfikuj klasę abstrakcyjną `Account`
    2. Dodaj anotację typów do wszystkich metod
    3. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isabstract, isclass, ismethod, signature

    >>> assert isclass(Account)
    >>> assert isabstract(Account)
    >>> assert hasattr(Account, '__init__')
    >>> assert hasattr(Account, 'login')
    >>> assert hasattr(Account, 'logout')
    >>> assert hasattr(Account.__init__, '__isabstractmethod__')
    >>> assert hasattr(Account.login, '__isabstractmethod__')
    >>> assert hasattr(Account.logout, '__isabstractmethod__')
    >>> assert Account.__init__.__isabstractmethod__ == True
    >>> assert Account.login.__isabstractmethod__ == True
    >>> assert Account.logout.__isabstractmethod__ == True

    >>> Account.__annotations__
    {'username': <class 'str'>, 'password': <class 'str'>}

    >>> Account.__init__.__annotations__
    {'username': <class 'str'>, 'password': <class 'str'>, 'return': None}

    >>> Account.login.__annotations__
    {'return': None}

    >>> Account.logout.__annotations__
    {'return': None}
"""


from abc import ABC, abstractmethod


# Modify abstract class `Account`
# Add type annotations to all methods
class Account(ABC):
    username: str
    password: str

    @abstractmethod
    def __init__(self, username, password):
        ...

    @abstractmethod
    def login(self):
        ...

    @abstractmethod
    def logout(self):
        ...


Code 6.49. Solution
"""
* Assignment: OOP AbstractClass Implement
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min

English:
    1. Define class `User` implementing `Account`
    2. Overwrite all abstract methods
    3. Leave `pass` as content
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `User` implementującą `Account`
    2. Nadpisz wszystkie metody abstrakcyjne
    3. Pozostaw `pass` jako treść
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isabstract, isclass, ismethod, signature

    >>> assert isclass(Account)
    >>> assert isabstract(Account)
    >>> assert hasattr(Account, '__init__')
    >>> assert hasattr(Account, 'login')
    >>> assert hasattr(Account, 'logout')
    >>> assert hasattr(Account.__init__, '__isabstractmethod__')
    >>> assert hasattr(Account.login, '__isabstractmethod__')
    >>> assert hasattr(Account.logout, '__isabstractmethod__')
    >>> assert Account.__init__.__isabstractmethod__ == True
    >>> assert Account.login.__isabstractmethod__ == True
    >>> assert Account.logout.__isabstractmethod__ == True

    >>> Account.__annotations__
    {'username': <class 'str'>, 'password': <class 'str'>}

    >>> Account.__init__.__annotations__
    {'username': <class 'str'>, 'password': <class 'str'>, 'return': None}

    >>> Account.login.__annotations__
    {'return': None}

    >>> Account.logout.__annotations__
    {'return': None}

    >>> assert isclass(User)
    >>> result = User(username='mwatney', password='Ares3')

    >>> result.__annotations__
    {'username': <class 'str'>, 'password': <class 'str'>}

    >>> assert hasattr(result, '__init__')
    >>> assert hasattr(result, 'logout')
    >>> assert hasattr(result, 'login')

    >>> assert ismethod(result.__init__)
    >>> assert ismethod(result.logout)
    >>> assert ismethod(result.login)

    >>> signature(result.__init__)  # doctest: +NORMALIZE_WHITESPACE
    <Signature (username: str, password: str) -> None>
    >>> signature(result.logout)
    <Signature () -> None>
    >>> signature(result.login)
    <Signature () -> None>

    >>> assert vars(result) == {}, 'Do not implement __init__() method'
    >>> assert result.login() is None, 'Do not implement login() method'
    >>> assert result.logout() is None, 'Do not implement logout() method'
"""

from abc import ABC, abstractmethod


class Account(ABC):
    username: str
    password: str

    @abstractmethod
    def __init__(self, username: str, password: str) -> None:
        ...

    @abstractmethod
    def login(self) -> None:
        ...

    @abstractmethod
    def logout(self) -> None:
        ...


# Define class `User` implementing `Account`
# Overwrite all abstract methods
# Leave `pass` as content
class User:
    ...