From c9a43e31c7cf9e31f2f9b804fda8c236ef4eb34b Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 21 Apr 2026 12:34:39 +0000 Subject: [PATCH 1/4] Sample code for: Implementing Interfaces in Python: ABCs and Protocols --- python-interface/README.md | 3 ++ python-interface/check_protocols.py | 11 +++++++ python-interface/check_virtual_classes.py | 5 +++ python-interface/create_readers.py | 7 +++++ python-interface/meta_classes.py | 37 ++++++++++++++++++++++ python-interface/readers.py | 36 +++++++++++++++++++++ python-interface/readers_abc.py | 37 ++++++++++++++++++++++ python-interface/readers_protocol.py | 38 +++++++++++++++++++++++ python-interface/registered_subclass.py | 12 +++++++ python-interface/virtual_subclass.py | 8 +++++ 10 files changed, 194 insertions(+) create mode 100644 python-interface/README.md create mode 100644 python-interface/check_protocols.py create mode 100644 python-interface/check_virtual_classes.py create mode 100644 python-interface/create_readers.py create mode 100644 python-interface/meta_classes.py create mode 100644 python-interface/readers.py create mode 100644 python-interface/readers_abc.py create mode 100644 python-interface/readers_protocol.py create mode 100644 python-interface/registered_subclass.py create mode 100644 python-interface/virtual_subclass.py diff --git a/python-interface/README.md b/python-interface/README.md new file mode 100644 index 0000000000..1190005ef7 --- /dev/null +++ b/python-interface/README.md @@ -0,0 +1,3 @@ +# Implementing Interfaces in Python: ABCs and Protocols + +This folder provides the code examples for the Real Python tutorial [Implementing Interfaces in Python: ABCs and Protocols](https://realpython.com/python-interface/) diff --git a/python-interface/check_protocols.py b/python-interface/check_protocols.py new file mode 100644 index 0000000000..dd6fa8c9a1 --- /dev/null +++ b/python-interface/check_protocols.py @@ -0,0 +1,11 @@ +from readers_protocols import FileReaderProtocol, PdfReader, EmailReader + + +def read(reader: FileReaderProtocol, path: str) -> str: + reader.load_file(path) + return reader.extract_text() + + +# Accepted by the type checker +read(PdfReader(), "/reports/report.pdf") +read(EmailReader(), "/mail/message.eml") diff --git a/python-interface/check_virtual_classes.py b/python-interface/check_virtual_classes.py new file mode 100644 index 0000000000..6aa427bc8e --- /dev/null +++ b/python-interface/check_virtual_classes.py @@ -0,0 +1,5 @@ +from meta_classes import EmailReader, PdfReader, VirtualFileReader + +print(isinstance(PdfReader(), VirtualFileReader)) +print(issubclass(PdfReader, VirtualFileReader)) +print(issubclass(EmailReader, VirtualFileReader)) diff --git a/python-interface/create_readers.py b/python-interface/create_readers.py new file mode 100644 index 0000000000..a023b9be8a --- /dev/null +++ b/python-interface/create_readers.py @@ -0,0 +1,7 @@ +from readers_abc import EmailReader, FileReaderInterface, PdfReader + +pdf_reader = PdfReader() +email_reader = EmailReader() + +print(isinstance(PdfReader(), FileReaderInterface)) +print(issubclass(PdfReader, FileReaderInterface)) diff --git a/python-interface/meta_classes.py b/python-interface/meta_classes.py new file mode 100644 index 0000000000..dcf22c0e7e --- /dev/null +++ b/python-interface/meta_classes.py @@ -0,0 +1,37 @@ +class ReaderMeta(type): + """A Reader metaclass used for reader class creation.""" + + def __instancecheck__(cls, instance): + return cls.__subclasscheck__(type(instance)) + + def __subclasscheck__(cls, subclass): + return ( + hasattr(subclass, "load_file") + and callable(subclass.load_file) + and hasattr(subclass, "extract_text") + and callable(subclass.extract_text) + ) + + +class VirtualFileReader(metaclass=ReaderMeta): + """Virtual class.""" + + +class PdfReader: + """Extract text from a PDF.""" + + def load_file(self, path: str) -> None: + print(f"Loading PDF from {path}") + + def extract_text(self) -> str: + return "Extracted PDF text" + + +class EmailReader: + """Extract text from an Email.""" + + def load_file(self, path: str) -> None: + print(f"Loading Email from {path}") + + def extract_email_text(self) -> str: + return "Extracted Email text" diff --git a/python-interface/readers.py b/python-interface/readers.py new file mode 100644 index 0000000000..751387c1b7 --- /dev/null +++ b/python-interface/readers.py @@ -0,0 +1,36 @@ +class PdfReader: + """Extract text from a PDF.""" + + def load_file(self, path: str) -> None: + print(f"Loading PDF from {path}") + + def extract_text(self) -> str: + return "Extracted PDF text" + + +class EmailReader: + """Extract text from an Email.""" + + def load_file(self, path: str) -> None: + print(f"Loading Email from {path}") + + def extract_text(self) -> str: + return "Extracted Email text" + + +def read(reader, path: str) -> str: + reader.load_file(path) + return reader.extract_text() + + +print(read(PdfReader(), "/reports/report.pdf")) +print(read(EmailReader(), "/mail/message.eml")) + +# class EmailReader: +# """Extract text from an Email.""" + +# def load_file(self, path: str) -> None: +# print(f"Loading Email from {path}") + +# def extract_email_text(self) -> str: +# return "Extracted Email text" diff --git a/python-interface/readers_abc.py b/python-interface/readers_abc.py new file mode 100644 index 0000000000..4c13bc959c --- /dev/null +++ b/python-interface/readers_abc.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod + + +class FileReaderInterface(ABC): + """Interface for file readers.""" + + @abstractmethod + def load_file(self, path: str) -> None: + """Load a file for text extraction.""" + + @abstractmethod + def extract_text(self) -> str: + """Return text extracted from the loaded file.""" + + +class PdfReader(FileReaderInterface): + """Extract text from a PDF.""" + + def load_file(self, path: str) -> None: + """Load a PDF file for text extraction.""" + print(f"Loading PDF from {path}") + + def extract_text(self) -> str: + """Return text extracted from the loaded PDF.""" + return "Extracted PDF text" + + +class EmailReader(FileReaderInterface): + """Extract text from an Email.""" + + def load_file(self, path: str) -> None: + """Load an EML file for text extraction.""" + print(f"Loading Email from {path}") + + def extract_email_text(self) -> str: + """Return text extracted from the loaded Email.""" + return "Extracted Email text" diff --git a/python-interface/readers_protocol.py b/python-interface/readers_protocol.py new file mode 100644 index 0000000000..d564fcd164 --- /dev/null +++ b/python-interface/readers_protocol.py @@ -0,0 +1,38 @@ +from typing import Protocol + + +# @runtime_checkable # Uncomment for runtime checks of protocol adherence +class FileReaderProtocol(Protocol): + """Protocol for file readers.""" + + def load_file(self, path: str) -> None: + """Load a file for text extraction.""" + ... + + def extract_text(self) -> str: + """Return text extracted from the loaded file.""" + ... + + +class PdfReader: + """Extract text from a PDF.""" + + def load_file(self, path: str) -> None: + """Load a PDF file for text extraction.""" + print("Loading your PDF...") + + def extract_text(self) -> str: + """Return text extracted from the loaded PDF.""" + return "Your PDF content" + + +class EmailReader: + """Extract text from an Email.""" + + def load_file(self, path: str) -> None: + """Load an EML file for text extraction.""" + print("Loading your Email...") + + def extract_text(self) -> str: + """Return text extracted from the loaded Email.""" + return "Your Email content" diff --git a/python-interface/registered_subclass.py b/python-interface/registered_subclass.py new file mode 100644 index 0000000000..6aa44811ef --- /dev/null +++ b/python-interface/registered_subclass.py @@ -0,0 +1,12 @@ +from virtual_subclass import Double + +print(issubclass(float, Double)) +print(isinstance(1.2345, Double)) + + +@Double.register +class Double64: + """A 64-bit double-precision floating-point number.""" + + +print(issubclass(Double64, Double)) diff --git a/python-interface/virtual_subclass.py b/python-interface/virtual_subclass.py new file mode 100644 index 0000000000..7f4e55f38b --- /dev/null +++ b/python-interface/virtual_subclass.py @@ -0,0 +1,8 @@ +from abc import ABC + + +class Double(ABC): + """Double precision floating point number.""" + + +Double.register(float) From e3f22a0cd916ad71f594565a113eb8b369b587e7 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 19 May 2026 08:53:31 +0000 Subject: [PATCH 2/4] Sync python-interface materials with tutorial update - Fix typo in check_protocols.py import (readers_protocols -> readers_protocol). - Import runtime_checkable in readers_protocol.py so uncommenting the decorator doesn't NameError. - Remove meta_classes.py and check_virtual_classes.py: the metaclass-based virtual subclass example was dropped from the updated tutorial in favor of typing.Protocol + @runtime_checkable. Co-Authored-By: Claude Opus 4.7 (1M context) --- python-interface/check_protocols.py | 2 +- python-interface/check_virtual_classes.py | 5 --- python-interface/meta_classes.py | 37 ----------------------- python-interface/readers_protocol.py | 4 +-- 4 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 python-interface/check_virtual_classes.py delete mode 100644 python-interface/meta_classes.py diff --git a/python-interface/check_protocols.py b/python-interface/check_protocols.py index dd6fa8c9a1..3d9da7fdd3 100644 --- a/python-interface/check_protocols.py +++ b/python-interface/check_protocols.py @@ -1,4 +1,4 @@ -from readers_protocols import FileReaderProtocol, PdfReader, EmailReader +from readers_protocol import FileReaderProtocol, PdfReader, EmailReader def read(reader: FileReaderProtocol, path: str) -> str: diff --git a/python-interface/check_virtual_classes.py b/python-interface/check_virtual_classes.py deleted file mode 100644 index 6aa427bc8e..0000000000 --- a/python-interface/check_virtual_classes.py +++ /dev/null @@ -1,5 +0,0 @@ -from meta_classes import EmailReader, PdfReader, VirtualFileReader - -print(isinstance(PdfReader(), VirtualFileReader)) -print(issubclass(PdfReader, VirtualFileReader)) -print(issubclass(EmailReader, VirtualFileReader)) diff --git a/python-interface/meta_classes.py b/python-interface/meta_classes.py deleted file mode 100644 index dcf22c0e7e..0000000000 --- a/python-interface/meta_classes.py +++ /dev/null @@ -1,37 +0,0 @@ -class ReaderMeta(type): - """A Reader metaclass used for reader class creation.""" - - def __instancecheck__(cls, instance): - return cls.__subclasscheck__(type(instance)) - - def __subclasscheck__(cls, subclass): - return ( - hasattr(subclass, "load_file") - and callable(subclass.load_file) - and hasattr(subclass, "extract_text") - and callable(subclass.extract_text) - ) - - -class VirtualFileReader(metaclass=ReaderMeta): - """Virtual class.""" - - -class PdfReader: - """Extract text from a PDF.""" - - def load_file(self, path: str) -> None: - print(f"Loading PDF from {path}") - - def extract_text(self) -> str: - return "Extracted PDF text" - - -class EmailReader: - """Extract text from an Email.""" - - def load_file(self, path: str) -> None: - print(f"Loading Email from {path}") - - def extract_email_text(self) -> str: - return "Extracted Email text" diff --git a/python-interface/readers_protocol.py b/python-interface/readers_protocol.py index d564fcd164..d171194048 100644 --- a/python-interface/readers_protocol.py +++ b/python-interface/readers_protocol.py @@ -1,7 +1,7 @@ -from typing import Protocol +from typing import Protocol, runtime_checkable -# @runtime_checkable # Uncomment for runtime checks of protocol adherence +# @runtime_checkable # Uncomment for runtime checks of protocol adherence class FileReaderProtocol(Protocol): """Protocol for file readers.""" From 8adad23e3fc732b52e62aa1d95a6143a2efee763 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Tue, 19 May 2026 08:59:32 +0000 Subject: [PATCH 3/4] Enable @runtime_checkable in readers_protocol.py Matches the tutorial's final state (highlighted block in the Runtime Type-Checking Protocol Subtypes section) and silences the F401 ruff error for the unused runtime_checkable import. Co-Authored-By: Claude Opus 4.7 (1M context) --- python-interface/readers_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-interface/readers_protocol.py b/python-interface/readers_protocol.py index d171194048..1d54024a5b 100644 --- a/python-interface/readers_protocol.py +++ b/python-interface/readers_protocol.py @@ -1,7 +1,7 @@ from typing import Protocol, runtime_checkable -# @runtime_checkable # Uncomment for runtime checks of protocol adherence +@runtime_checkable class FileReaderProtocol(Protocol): """Protocol for file readers.""" From 0ff11be3b7c9a52e3a3892790614323a20bee9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Sat, 30 May 2026 16:11:54 +0200 Subject: [PATCH 4/4] Final QA --- python-interface/check_protocols.py | 2 +- python-interface/readers.py | 16 ++++++++-------- python-interface/readers_abc.py | 6 +++--- python-interface/readers_protocol.py | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python-interface/check_protocols.py b/python-interface/check_protocols.py index 3d9da7fdd3..94d4007570 100644 --- a/python-interface/check_protocols.py +++ b/python-interface/check_protocols.py @@ -1,4 +1,4 @@ -from readers_protocol import FileReaderProtocol, PdfReader, EmailReader +from readers_protocol import EmailReader, FileReaderProtocol, PdfReader def read(reader: FileReaderProtocol, path: str) -> str: diff --git a/python-interface/readers.py b/python-interface/readers.py index 751387c1b7..ad76c83a4e 100644 --- a/python-interface/readers.py +++ b/python-interface/readers.py @@ -9,13 +9,13 @@ def extract_text(self) -> str: class EmailReader: - """Extract text from an Email.""" + """Extract text from an email.""" def load_file(self, path: str) -> None: - print(f"Loading Email from {path}") + print(f"Loading email from {path}") def extract_text(self) -> str: - return "Extracted Email text" + return "Extracted email text" def read(reader, path: str) -> str: @@ -27,10 +27,10 @@ def read(reader, path: str) -> str: print(read(EmailReader(), "/mail/message.eml")) # class EmailReader: -# """Extract text from an Email.""" - +# """Extract text from an email.""" +# # def load_file(self, path: str) -> None: -# print(f"Loading Email from {path}") - +# print(f"Loading email from {path}") +# # def extract_email_text(self) -> str: -# return "Extracted Email text" +# return "Extracted email text" diff --git a/python-interface/readers_abc.py b/python-interface/readers_abc.py index 4c13bc959c..f795cb3f9f 100644 --- a/python-interface/readers_abc.py +++ b/python-interface/readers_abc.py @@ -30,8 +30,8 @@ class EmailReader(FileReaderInterface): def load_file(self, path: str) -> None: """Load an EML file for text extraction.""" - print(f"Loading Email from {path}") + print(f"Loading email from {path}") def extract_email_text(self) -> str: - """Return text extracted from the loaded Email.""" - return "Extracted Email text" + """Return text extracted from the loaded email.""" + return "Extracted email text" diff --git a/python-interface/readers_protocol.py b/python-interface/readers_protocol.py index 1d54024a5b..256a3c1a22 100644 --- a/python-interface/readers_protocol.py +++ b/python-interface/readers_protocol.py @@ -19,11 +19,11 @@ class PdfReader: def load_file(self, path: str) -> None: """Load a PDF file for text extraction.""" - print("Loading your PDF...") + print(f"Loading PDF from {path}") def extract_text(self) -> str: """Return text extracted from the loaded PDF.""" - return "Your PDF content" + return "Extracted PDF text" class EmailReader: @@ -31,8 +31,8 @@ class EmailReader: def load_file(self, path: str) -> None: """Load an EML file for text extraction.""" - print("Loading your Email...") + print(f"Loading email from {path}") def extract_text(self) -> str: - """Return text extracted from the loaded Email.""" - return "Your Email content" + """Return text extracted from the loaded email.""" + return "Extracted email text"