1.1. Abstract Interface

  • Python don't have interfaces, although you can achieve similar effect

  • Since Python 3.8 there are Protocols, which effectively are interfaces

  • Interfaces cannot be instantiated

  • Interfaces can be implemented

  • Implemented class must define all interface methods (implement interface)

  • Only public method declaration

interface

Software entity with public methods and attribute declaration

implement

Class implements interface if has all public fields and methods from interface

How do you specify and enforce an interface spec in Python?

An interface specification for a module as provided by languages such as C++ and Java describes the prototypes for the methods and functions of the module. Many feel that compile-time enforcement of interface specifications helps in the construction of large programs.

Python 3.0 adds an abc module that lets you define Abstract Base Classes (ABCs). You can then use isinstance() and issubclass() to check whether an instance or a class implements a particular ABC. The collections.abc module defines a set of useful abstract base classes such as Iterable, Container, and MutableMapping.

For Python, many of the advantages of interface specifications can be obtained by an appropriate test discipline for components.

A good test suite for a module can both provide a regression test and serve as a module interface specification and a set of examples. Many Python modules can be run as a script to provide a simple "self test". Even modules which use complex external interfaces can often be tested in isolation using trivial "stub" emulations of the external interface. The doctest and unittest modules or third-party test frameworks can be used to construct exhaustive test suites that exercise every line of code in a module.

An appropriate testing discipline can help build large complex applications in Python as well as having interface specifications would. In fact, it can be better because an interface specification cannot test certain properties of a program. For example, the append() method is expected to add new elements to the end of some internal list; an interface specification cannot test that your append() implementation will actually do this correctly, but it's trivial to check this property in a test suite.

Writing test suites is very helpful, and you might want to design your code to make it easily tested. One increasingly popular technique, test-driven development, calls for writing parts of the test suite first, before you write any of the actual code. Of course Python allows you to be sloppy and not write test cases at all.

Note

Source [2]

1.1.1. Problem

  • Different method names

  • Different method return type

  • Different parameter names

  • Different parameter types

>>> class User:
...     def login(self): ...
...     def logout(self): ...
...
>>> class Admin:
...     def log_in(self): ...
...     def log_out(self): ...

Each of those classes has different names for methods which eventually does the same job. This is lack of consistency and common interface:

>>> mark = User()
>>> mark.login()
>>> mark.logout()
>>> mark = Admin()
>>> mark.log_in()
>>> mark.log_out()

1.1.2. Solution

  • Interface specifies method names, method return type, parameter names and types

  • This way, you can substitute one class with another without changing much code

>>> class Account:
...     def login(self): raise NotImplementedError
...     def logout(self): raise NotImplementedError
>>>
>>> class User(Account):
...     def login(self): ...
...     def logout(self): ...
>>>
>>> class Admin(Account):
...     def login(self): ...
...     def logout(self): ...

mark = User() mark.login() mark.logout()

mark = Admin() mark.login() mark.logout()

1.1.3. Dependency Inversion Principle

  • One of S.O.L.I.D. principles

  • DIP - Dependency Inversion Principle

  • Always depend on an abstraction not concrete implementation

The principle states: High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

—SOLID, Dependency Inversion Principle, Robert C. Martin

Good (depend on an abstraction):

>>> mark: Account = User()

Bad (depend on concrete implementation):

>>> mark: User = User()
>>> mark: Admin = Admin()

1.1.4. Interface Names

  • Account

  • IAccount

  • AccountIface

  • AccountInterface

>>> class Account:
...     ...
>>> class IAccount:
...     ...
>>> class AccountIface:
...     ...
>>> class AccountInterface:
...     ...

1.1.5. Syntax

  • raise NotImplementedError

>>> class Account:
...     def login(self):
...         raise NotImplementedError
...
...     def logout(self):
...         raise NotImplementedError

Interfaces do not have any implementation, so you can write them as one-liners. It is a bit more easier to read. You will also focus more on method names and attribute types.

>>> class Account:
...     def login(self): raise NotImplementedError
...     def logout(self): raise NotImplementedError

1.1.6. Other Languages

  • This currently does not exists in Python

  • In fact it is not even a valid Python syntax

  • But it could greatly improve readability

How nice it would be to write:

>>> @interface 
... class Account:
...     def login(self): ...
...     def logout(self): ...
>>> class Account(interface=True): 
...     def login(self): ...
...     def logout(self): ...
>>> interface Account: 
...     def login(self): ...
...     def logout(self): ...

1.1.7. Should Fields be in the Interface?

  • Generally no, but...

  • Encapsulation requires state to be hidden inside of a class

  • Therefore all fields should be private, and there should be public methods to manipulate them

  • Public methods should be in the interface

  • Although in Python, all fields are public, and we set their values directly without setters and getters

  • Then, they should be in the interface (this is controversial)

>>> from datetime import timedelta
>>>
>>>
>>> class Cache:
...     timeout: timedelta = ...
...     location: str | None = ...
...     def __init__(self, timeout: timedelta) -> None: raise NotImplementedError
...     def set(self, key: str, value: str) -> None: raise NotImplementedError
...     def get(self, key: str) -> str: raise NotImplementedError
...     def delete(self, key: str) -> None: raise NotImplementedError
>>>
>>>
>>> class MemoryCache(Cache):
...     timeout = 10
...     location = 'myapp'
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class FilesystemCache(Cache):
...     timeout = 30
...     location = '/tmp/myapp'
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class DatabaseCache(Cache):
...     timeout = 300
...     location = 'myapp.cache'
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...

Alternative (__init__ parameter):

>>> class Cache:
...     def __init__(self, timeout: timedelta, location: str) -> None: raise NotImplementedError
...     def set(self, key: str, value: str) -> None: raise NotImplementedError
...     def get(self, key: str) -> str: raise NotImplementedError
...     def delete(self, key: str) -> None: raise NotImplementedError
>>>
>>>
>>> class MemoryCache(Cache):
...     def __init__(self, timeout: timedelta, location: str) -> None: ...
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class FilesystemCache(Cache):
...     def __init__(self, timeout: timedelta, location: str) -> None: ...
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class DatabaseCache(Cache):
...     def __init__(self, timeout: timedelta, location: str) -> None: ...
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...

1.1.8. Interface vs. Abstract Class

  • Abstract class: Regular classes and you can inherit from them

  • Abstract class: This allows to have a default implementation of some methods

  • Abstract class: In the child class only abstract methods have to be implemented

  • Abstract class: Validation is dynamic (in the runtime)

  • Interface: Validation is static (without running the code)

  • Interface: Cannot have methods with implementation

  • Interface: Contain only methods names and returning types, parameter names and types

Abstract class:

>>> from abc import ABC, abstractmethod
>>>
>>> class Account(ABC):
...     @abstractmethod
...     def login(self):
...         raise NotImplementedError
...
...     @abstractmethod
...     def logout(self):
...         raise NotImplementedError
...
...     def hello(self):
...         print('hello')

Interface:

>>> class Account:
...     def login(self): raise NotImplementedError
...     def logout(self): raise NotImplementedError

1.1.9. Case Study

class MemoryCache:
    def store(self, key: str, value: str) -> None: ...
    def retrieve(self, key: str) -> str: ...
    def free(self, key: str) -> None: ...


mycache = MemoryCache()
mycache.store('firstname', 'Mark')
mycache.retrieve('firstname')
mycache.free()
class MemoryCache:
    def store(self, key: str, value: str) -> None: ...
    def retrieve(self, key: str) -> str: ...
    def free(self, key: str) -> None: ...

class FilesystemCache:
    def write(self, key: str, value: str) -> None: ...
    def read(self, key: str) -> str: ...
    def unlink(self, key: str) -> None: ...


mycache = MemoryCache()
mycache.store('firstname', 'Mark')
mycache.retrieve('firstname')
mycache.free()

mycache = FilesystemCache()
mycache.write('firstname', 'Mark')
mycache.read('firstname')
mycache.unlink()
class MemoryCache:
    def store(self, key: str, value: str) -> None: ...
    def retrieve(self, key: str) -> str: ...
    def free(self, key: str) -> None: ...

class FilesystemCache:
    def write(self, key: str, value: str) -> None: ...
    def read(self, key: str) -> str: ...
    def unlink(self, key: str) -> None: ...

class DatabaseCache:
    def insert(self, key: str, value: str) -> None: ...
    def select(self, key: str) -> str: ...
    def delete(self, key: str) -> None: ...


mycache = MemoryCache()
mycache.store('firstname', 'Mark')
mycache.retrieve('firstname')
mycache.free('firstname')

mycache = FilesystemCache()
mycache.write('firstname', 'Mark')
mycache.read('firstname')
mycache.unlink('firstname')

mycache = DatabaseCache()
mycache.insert('firstname', 'Mark')
mycache.select('firstname')
mycache.delete('firstname')

1.1.10. Use Case - 1

>>> class Account:
...     def login(self, username: str, password: str) -> None: ...
...     def logout(self) -> None: ...
>>>
>>>
>>> class Guest(Account):
...     def login(self, username: str, password: str) -> None: ...
...     def logout(self) -> None: ...
>>>
>>> class User(Account):
...     def login(self, username: str, password: str) -> None: ...
...     def logout(self) -> None: ...
>>>
>>> class Admin(Account):
...     def login(self, username: str, password: str) -> None: ...
...     def logout(self) -> None: ...

1.1.11. Use Case - 2

>>> class Cache:
...     def set(self, key: str, value: str) -> None: raise NotImplementedError
...     def get(self, key: str) -> str: raise NotImplementedError
...     def delete(self, key: str) -> None: raise NotImplementedError
>>>
>>>
>>> class DatabaseCache(Cache):
...     table = 'cache'
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class FilesystemCache(Cache):
...     basedir = '/tmp'
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>> class MemoryCache(Cache):
...     timeout = 5
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def delete(self, key: str) -> None: ...
>>>
>>>
>>> mycache: Cache = MemoryCache()
>>> mycache.set('firstname', 'Mark')
>>> mycache.set('lastname', 'Watney')
>>> mycache.get('firstname')
>>> mycache.get('lastname')
>>> mycache.delete('firstname')
>>> mycache.delete('lastname')

1.1.12. Use Case - 3

File cache.py:

>>> class Cache:
...     def get(self, key: str) -> str: raise NotImplementedError
...     def set(self, key: str, value: str) -> None: raise NotImplementedError
...     def clear(self) -> None: raise NotImplementedError
>>>
>>>
>>> class DatabaseCache(Cache):
...     def get(self, key: str) -> str: ...
...     def set(self, key: str, value: str) -> None: ...
...     def clear(self) -> None: ...
>>>
>>> class MemoryCache(Cache):
...     def get(self, key: str) -> str: ...
...     def set(self, key: str, value: str) -> None: ...
...     def clear(self) -> None: ...
>>>
>>> class FilesystemCache(Cache):
...     def get(self, key: str) -> str: ...
...     def set(self, key: str, value: str) -> None: ...
...     def clear(self) -> None: ...

Operating system:

$ export CACHE=DatabaseCache

File settings.py

>>> 
... from os import getenv
... from myapp.cache import Cache
... import myapp.cache
...
...
... CACHE = getenv('CACHE', default='MemoryCache')
... DefaultCache: Cache = getattr(myapp.cache, CACHE)

File myapp.py:

>>> 
... from myapp.settings import DefaultCache, Cache
...
...
... mycache: Cache = DefaultCache()
... mycache.set('firstname', 'Mark')
... mycache.set('lastname', 'Watney')
... mycache.get('firstname')
... mycache.get('lastname')
... mycache.clear()

Note, that myapp doesn't know which cache is being used. It only depends on environmental variable and not even a settings file.

1.1.13. Use Case - 3

../../_images/oop-interface-gimp.jpg

Figure 1.12. GIMP (GNU Image Manipulation Project) window with tools and canvas [1]

Interface definition with all event handler specification:

>>> class Tool:
...     def on_mouse_over(self): raise NotImplementedError
...     def on_mouse_out(self): raise NotImplementedError
...     def on_mouse_leftbutton(self): raise NotImplementedError
...     def on_mouse_rightbutton(self): raise NotImplementedError
...     def on_key_press(self): raise NotImplementedError
...     def on_key_unpress(self): raise NotImplementedError

Implementation:

>>> class Pencil(Tool):
...     def on_mouse_over(self): ...
...     def on_mouse_out(self): ...
...     def on_mouse_leftbutton(self): ...
...     def on_mouse_rightbutton(self): ...
...     def on_key_press(self): ...
...     def on_key_unpress(self): ...
>>>
>>>
>>> class Pen(Tool):
...     def on_mouse_over(self): ...
...     def on_mouse_out(self): ...
...     def on_mouse_leftbutton(self): ...
...     def on_mouse_rightbutton(self): ...
...     def on_key_press(self): ...
...     def on_key_unpress(self): ...
>>>
>>>
>>> class Brush(Tool):
...     def on_mouse_over(self): ...
...     def on_mouse_out(self): ...
...     def on_mouse_leftbutton(self): ...
...     def on_mouse_rightbutton(self): ...
...     def on_key_press(self): ...
...     def on_key_unpress(self): ...
>>>
>>>
>>> class Eraser(Tool):
...     def on_mouse_over(self): ...
...     def on_mouse_out(self): ...
...     def on_mouse_leftbutton(self): ...
...     def on_mouse_rightbutton(self): ...
...     def on_key_press(self): ...
...     def on_key_unpress(self): ...

1.1.14. References

1.1.15. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: OOP AbstractInterface Define
# - Difficulty: easy
# - Lines: 9
# - Minutes: 5

# %% English
# 1. Define interface `Account` with:
#    - Methods: `__init__()`, `login()`, `logout()`
#    - Init parameters: `username`, `password`
# 2. All methods must raise exception `NotImplementedError`
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj interfejs `Account` z:
#    - Metody: `__init__()`, `login()`, `logout()`
#    - Parametry init: `username`, `password`
# 2. Wszystkie metody muszą podnosić wyjątek `NotImplementedError`
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction

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

>>> assert isfunction(Account.__init__)
>>> assert isfunction(Account.login)
>>> assert isfunction(Account.logout)

>>> mark = Account(username='mwatney', password='Ares3')
Traceback (most recent call last):
NotImplementedError

>>> Account.login(None)
Traceback (most recent call last):
NotImplementedError

>>> Account.logout(None)
Traceback (most recent call last):
NotImplementedError
"""

# Define interface `Account` with:
# - Methods: `__init__()`, `login()`, `logout()`
# - Init parameters: `username`, `password`
# All methods must raise exception `NotImplementedError`
# type: type[Account]
...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: OOP AbstractInterface Implement
# - Difficulty: easy
# - Lines: 10
# - Minutes: 5

# %% English
# 1. Define class `User` implementing `Account`
# 2. Implement methods:
#    - `__init__()` sets fields
#    - `login()` returns 'User login'
#    - `logout()` returns 'User logout'
# 3. Run doctests - all must succeed

# %% Polish
# 1. Stwórz klasę `User` implementującą `Account`
# 2. Zaimplementuj metody:
#    - `__init__` ustawia pola klasy
#    - `login()` zwraca 'User login'
#    - `logout()` zwraca 'User logout'
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `vars(self).values()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isfunction

>>> assert issubclass(User, Account)
>>> assert hasattr(User, '__init__')
>>> assert hasattr(User, 'login')
>>> assert hasattr(User, 'logout')

>>> assert isfunction(User.__init__)
>>> assert isfunction(User.login)
>>> assert isfunction(User.logout)

>>> User.__annotations__  # doctest: +NORMALIZE_WHITESPACE
{'username': <class 'str'>,
 'password': <class 'str'>}

>>> mark = User(username='mwatney', password='Ares3')
>>> mark.login()
'User login'
>>> mark.logout()
'User logout'
"""

class Account:
    def __init__(self, username: str, password: str) -> None:
        raise NotImplementedError

    def login(self) -> str:
        raise NotImplementedError

    def logout(self) -> str:
        raise NotImplementedError


# Define class `User` implementing `Account`
# Implement methods:
# - `__init__()` sets fields
# - `login()` returns 'User login'
# - `logout()` returns 'User logout'
# type: type[Account]
class User:
    ...