6.15. OOP 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.15.1. SetUp

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

6.15.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, username: str, password: str) -> 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 with abstract method login

6.15.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):
...     @abstractmethod
...     def login(self, username: str, password: str) -> None:
...         raise NotImplementedError
...
...     @abstractmethod
...     def logout(self) -> None:
...         raise NotImplementedError
...
...     def say_hello(self):
...         return 'hello'

Implementation:

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

Use:

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

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.15.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):
...     @abstractmethod
...     def login(self, username: str, password: str) -> None:
...         raise NotImplementedError

6.15.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.15.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.15.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, username: str, password: str) -> 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.15.8. Problem: All Abstract Methods are not Implemented

Must implement all abstract methods:

>>> class Account(ABC):
...     @abstractmethod
...     def login(self, username: str, password: str) -> 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 with 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.15.9. Problem: Some Abstract Methods are not Implemented

All abstract methods must be implemented in child class:

>>> class Account(ABC):
...     @abstractmethod
...     def login(self, username: str, password: str) -> None:
...         raise NotImplementedError
...
...     @abstractmethod
...     def logout(self) -> None:
...         raise NotImplementedError
>>>
>>>
>>> class User(Account):
...     def login(self, username: str, password: str) -> None:
...         print('Logging-in')
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User with 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.15.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 with 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.15.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 with 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.15.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.15.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.15.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 with abstract method display

6.15.15. Use Case - 0x02

>>> from abc import ABC, abstractmethod
>>> class UIElement(ABC):
...     def __init__(self, name):
...         self.name = name
...
...     @abstractmethod
...     def render(self):
...         pass
>>>
>>>
>>> def render(component: list[UIElement]):
...     for element in component:
...         element.render()
>>> class TextInput(UIElement):
...     def render(self):
...         print(f'Rendering {self.name} TextInput')
>>>
>>>
>>> class Button(UIElement):
...     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.15.16. Use Case - 0x03

>>> class Person(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 Astronaut(Person):
...     AGE_MIN = 30
...     AGE_MAX = 50
>>> mark = Astronaut(age=40)

6.15.17. Further Reading

6.15.18. References

1

https://docs.python.org/3/library/collections.abc.html

6.15.19. Assignments

Code 6.30. Solution
"""
* Assignment: OOP AbstractClass Syntax
* Complexity: easy
* Lines of code: 10 lines
* Time: 5 min

English:
    1. Create abstract class `IrisAbstract`
    2. Create abstract method `get_name()` in `IrisAbstract`
    3. Create class `Setosa` inheriting from `IrisAbstract`
    4. Try to create instance of a class `Setosa`
    5. Try to create instance of a class `IrisAbstract`
    6. Run doctests - all must succeed

Polish:
    1. Stwórz klasę abstrakcyjną `IrisAbstract`
    2. Stwórz metodę abstrakcyjną `get_name()` w `IrisAbstract`
    3. Stwórz klasę `Setosa` dziedziczące po `IrisAbstract`
    4. Spróbuj stworzyć instancje klasy `Setosa`
    5. Spróbuj stworzyć instancję klasy `IrisAbstract`
    6. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(IrisAbstract)
    >>> assert isclass(Setosa)
    >>> assert isabstract(IrisAbstract)
    >>> assert not isabstract(Setosa)
    >>> assert hasattr(IrisAbstract, 'get_name')
    >>> assert hasattr(Setosa, 'get_name')
    >>> assert not hasattr(Setosa.get_name, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.get_name, '__isabstractmethod__')
    >>> assert IrisAbstract.get_name.__isabstractmethod__ == True

    >>> iris = IrisAbstract()
    Traceback (most recent call last):
    TypeError: Can't instantiate abstract class IrisAbstract with abstract method get_name
    >>> setosa = Setosa()
    >>> assert ismethod(setosa.get_name)

Warning:
    * Last line of doctest, second to last word of `TypeError` message
    * In Python 3.7, 3.8 there is "methods" word in doctest
    * In Python 3.9, 3.10 there is "method" word in doctest
    * So it differs by "s" at the end of "method" word
"""

Code 6.31. Solution
"""
* Assignment: OOP AbstractClass Interface
* Complexity: easy
* Lines of code: 11 lines
* Time: 5 min

English:
    1. Define abstract class `IrisAbstract`
    2. Abstract methods: `__init__`, `sum()`, `len()`, `mean()`
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę abstrakcyjną `IrisAbstract`
    2. Metody abstrakcyjne: `__init__`, `sum()`, `len()`, `mean()`
    3. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(IrisAbstract)
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert IrisAbstract.__init__.__isabstractmethod__ == True
    >>> assert IrisAbstract.mean.__isabstractmethod__ == True
    >>> assert IrisAbstract.sum.__isabstractmethod__ == True
    >>> assert IrisAbstract.len.__isabstractmethod__ == True

    >>> IrisAbstract.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>}
"""

from abc import ABC, abstractmethod


class IrisAbstract:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

    def __init__(self,
                 sepal_length: float,
                 sepal_width: float,
                 petal_length: float,
                 petal_width: float) -> None:
        ...

# Define abstract class `IrisAbstract`
# Abstract methods: `__init__`, `sum()`, `len()`, `mean()`


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

English:
    1. Modify abstract class `IrisAbstract`
    2. Add type annotation to all methods and attributes
    3. Run doctests - all must succeed

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

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

    >>> assert isclass(IrisAbstract)
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert hasattr(IrisAbstract.__init__, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.mean, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.sum, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.len, '__isabstractmethod__')
    >>> assert IrisAbstract.__init__.__isabstractmethod__ == True
    >>> assert IrisAbstract.mean.__isabstractmethod__ == True
    >>> assert IrisAbstract.sum.__isabstractmethod__ == True
    >>> assert IrisAbstract.len.__isabstractmethod__ == True

    >>> IrisAbstract.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>}

    >>> IrisAbstract.__init__.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>,
     'return': None}

     >>> IrisAbstract.mean.__annotations__
     {'return': <class 'float'>}

     >>> IrisAbstract.sum.__annotations__
     {'return': <class 'float'>}

     >>> IrisAbstract.len.__annotations__
     {'return': <class 'int'>}
"""

from abc import ABC, abstractmethod


class IrisAbstract(ABC):

    @abstractmethod
    def __init__(self, sepal_length, sepal_width, petal_length, petal_width):
        ...

    @abstractmethod
    def mean(self):
        ...

    @abstractmethod
    def sum(self):
        ...

    @abstractmethod
    def len(self):
        ...

# Modify abstract class `IrisAbstract`
# Add type annotation to all methods and attributes

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

English:
    1. Define class `Setosa` implementing `IrisAbstract`
    2. All method signatures must be identical to `IrisAbstract`
    3. Don't implement methods, leave `...` or `pass` as content
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Setosa` implementującą `IrisAbstract`
    2. Sygnatury wszystkich metod muszą być identyczne do `IrisAbstract`
    3. Nie implementuj metod, pozostaw `...` or `pass` jako zawartość
    4. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(IrisAbstract)
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert hasattr(IrisAbstract.__init__, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.mean, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.sum, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.len, '__isabstractmethod__')
    >>> assert IrisAbstract.__init__.__isabstractmethod__ == True
    >>> assert IrisAbstract.mean.__isabstractmethod__ == True
    >>> assert IrisAbstract.sum.__isabstractmethod__ == True
    >>> assert IrisAbstract.len.__isabstractmethod__ == True

    >>> IrisAbstract.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>}

    >>> IrisAbstract.__init__.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>,
     'return': None}

    >>> IrisAbstract.mean.__annotations__
    {'return': <class 'float'>}

    >>> IrisAbstract.sum.__annotations__
    {'return': <class 'float'>}

    >>> IrisAbstract.len.__annotations__
    {'return': <class 'int'>}

    >>> assert isclass(Setosa)
    >>> result = Setosa(5.1, 3.5, 1.4, 0.2)

    >>> result.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>, 'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>, 'petal_width': <class 'float'>}

    >>> assert hasattr(result, '__init__')
    >>> assert hasattr(result, 'len')
    >>> assert hasattr(result, 'sum')
    >>> assert hasattr(result, 'mean')

    >>> assert ismethod(result.__init__)
    >>> assert ismethod(result.len)
    >>> assert ismethod(result.sum)
    >>> assert ismethod(result.mean)

    >>> signature(result.__init__)  # doctest: +NORMALIZE_WHITESPACE
    <Signature (sepal_length: float, sepal_width: float, petal_length:
    float, petal_width: float) -> None>
    >>> signature(result.len)
    <Signature () -> int>
    >>> signature(result.sum)
    <Signature () -> float>
    >>> signature(result.mean)
    <Signature () -> float>

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

from abc import ABC, abstractmethod


class IrisAbstract(ABC):
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

    @abstractmethod
    def __init__(self,
                 sepal_length: float,
                 sepal_width: float,
                 petal_length: float,
                 petal_width: float) -> None:
        ...

    @abstractmethod
    def mean(self) -> float:
        ...

    @abstractmethod
    def sum(self) -> float:
        ...

    @abstractmethod
    def len(self) -> int:
        ...

# Define class `Setosa` implementing `IrisAbstract`
# Don't implement methods, leave `...` or `pass` as content