Skip to content
Python Basics: Defining Required Methods with abc

Python Basics: Defining Required Methods with abc

October 22, 2026

A report generator may offer several output styles while expecting every renderer to provide the same operation. Python’s abc module lets a base class mark required methods, so an incomplete ordinary subclass cannot be instantiated.

These examples build small in-memory renderers. They also show an important boundary: an abstract method requirement does not prove that an implementation produces correct output. Keep behavior checks alongside the class definition.

Prepare a scratch directory

These three standalone examples were tested on Linux with Python 3.12.14. The examples require Python 3.6 or newer because they use f-strings; the ABC helper itself was added in Python 3.4. Use Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Keep assertions enabled by running without Python’s -O option.

Create a fresh directory and keep using the same terminal:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each named source file here as UTF-8.

Avoid source filenames such as abc.py. The examples use invented labels and never write report files or contact a service.

1. Require a renderer implementation

Save as require_report_renderer.py:

from abc import ABC, abstractmethod


class ReportRenderer(ABC):
    @abstractmethod
    def render(self, names):
        raise NotImplementedError


class BulletRenderer(ReportRenderer):
    def render(self, names):
        return "\n".join(f"- {name}" for name in names)


try:
    ReportRenderer()
except TypeError:
    print("Incomplete renderer: rejected")
else:
    raise AssertionError("An abstract class was instantiated")

renderer = BulletRenderer()
assert renderer.render([]) == ""
assert renderer.render(["Build", "Test"]) == "- Build\n- Test"
print(renderer.render(["Build", "Test"]))
print("Empty report:", repr(renderer.render([])))

Run it:

python3 require_report_renderer.py

Output:

Incomplete renderer: rejected
- Build
- Test
Empty report: ''

ReportRenderer declares the operation, while BulletRenderer supplies the actual formatting. The failed construction is caught by type rather than by comparing Python’s full diagnostic, which can differ between releases.

The empty report is deliberately an empty string. The assertion for two names checks separators as well as content, so the test would catch an accidental trailing newline. An override with the wrong behavior could still make the class concrete; abc does not replace these tests.

2. Reuse validation from an abstract method

Save as reuse_renderer_validation.py:

from abc import ABC, abstractmethod


class LabelRenderer(ABC):
    @abstractmethod
    def render(self, label):
        cleaned = label.strip()
        if not cleaned:
            raise ValueError("label must not be blank")
        return cleaned


class BracketRenderer(LabelRenderer):
    def render(self, label):
        cleaned = super().render(label)
        return f"[{cleaned}]"


renderer = BracketRenderer()
assert renderer.render("  ready  ") == "[ready]"
print("Label:", renderer.render("  ready  "))
try:
    renderer.render("   ")
except ValueError as error:
    assert str(error) == "label must not be blank"
    print("Rejected:", error)
else:
    raise AssertionError("A blank label was accepted")

Run it:

python3 reuse_renderer_validation.py

Output:

Label: [ready]
Rejected: label must not be blank

An abstract method can contain useful code. BracketRenderer overrides render and explicitly calls the base implementation through super. The base strips whitespace and rejects a blank label; the subclass adds its own brackets.

This arrangement keeps one normalization rule while requiring each ordinary subclass to make a rendering choice. Calling super is still a convention that this subclass follows, not something abc forces every override to do. If validation must be unavoidable, put it in a concrete public method that calls a separate abstract formatting hook.

3. Check what virtual registration does not enforce

Save as register_legacy_renderer.py:

from abc import ABC, abstractmethod


class TextRenderer(ABC):
    @abstractmethod
    def render(self, text):
        raise NotImplementedError


class LegacyUppercase:
    def render(self, text):
        return text.upper()


class MissingRenderer:
    pass


TextRenderer.register(LegacyUppercase)
TextRenderer.register(MissingRenderer)
legacy = LegacyUppercase()
missing = MissingRenderer()
assert isinstance(legacy, TextRenderer)
assert issubclass(LegacyUppercase, TextRenderer)
assert legacy.render("ready") == "READY"
assert TextRenderer not in LegacyUppercase.__mro__
assert isinstance(missing, TextRenderer)
assert not hasattr(missing, "render")
print("Legacy output:", legacy.render("ready"))
print("Recognized:", isinstance(legacy, TextRenderer))
print("Incomplete registered object has render:", hasattr(missing, "render"))

Run it:

python3 register_legacy_renderer.py

Output:

Legacy output: READY
Recognized: True
Incomplete registered object has render: False

LegacyUppercase already has the operation but does not inherit from TextRenderer. Registration makes the isinstance and issubclass checks recognize it without changing its method-resolution order. It neither copies methods nor validates the implementation.

MissingRenderer demonstrates the risk deliberately: it is accepted by the type check but has no render method. Register an adapter only after checking its actual behavior. For a new class under your control, ordinary inheritance is usually easier to inspect than a registration elsewhere in the program.

Use an abstract base when several implementations need a clearly named contract. For one small function, a class hierarchy may add more structure than the task needs. Keep the required methods few and give each implementation the same behavioral tests.

Clean up

In the same terminal, remove only these source files, return to the original directory, and remove the now-empty scratch directory:

rm -- require_report_renderer.py reuse_renderer_validation.py register_legacy_renderer.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports that anything remains, inspect the directory rather than deleting unknown files.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on