Filter concepts by levelShowing all levels.

Python · Object-Oriented Python

Abstract classes and interfaces

Concepts
6

How to require a method with abc — versus how Python already lets any matching shape stand in without any inheritance at all, and how typing.Protocol makes that shape a type checker can verify.

This section

The abc module

How Python enforces a required method, and what actually does the enforcing.

The abc module

standardbeginner

abc is the standard-library module behind Python's abstract base classes — it supplies the ABC base class and the @abstractmethod decorator that together stop a class from being instantiated until it fills in every required method.

Think of it as

abc is the module a form-template system is built from — ABCMeta is the printer that refuses to hand out a blank form with required fields still missing. ABC and @abstractmethod are the two pieces most code actually reaches for; ABCMeta is the machinery underneath both, rarely used directly.

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

What we're doing: Define an abstract base class with one required method, then show that Python itself refuses to instantiate it directly.

shape.pypython
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2


print(Circle(2).area())
print(Shape())
3
class Shape(ABC): opts into abc's enforcement — without this, @abstractmethod below would be decorative only.
4
@abstractmethod marks area as required — any subclass that skips it cannot be instantiated either.
9
Circle defines area, so it satisfies the requirement and can be instantiated normally.
Error
12.56636
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'

Why this works: Circle(2).area() works because Circle provides a concrete area, satisfying the one requirement Shape declared. Shape() itself fails at the instantiation step — before __init__ even runs — because Python's ABCMeta machinery checks for unimplemented abstract methods and refuses to build the object at all, rather than letting it fail later when area() is actually called.

Forgetting to inherit from ABC and expecting @abstractmethod to still enforce anything

Wrong

python
from abc import abstractmethod

class Shape:                 # NOT ABC — no enforcement
    @abstractmethod
    def area(self):
        ...

s = Shape()                  # no error!
print(s)

Better

python
from abc import ABC, abstractmethod

class Shape(ABC):            # ABC is what enforces it
    @abstractmethod
    def area(self):
        ...

s = Shape()                  # TypeError, as intended

What you see: Shape() succeeds silently — @abstractmethod alone does nothing; the check lives in ABCMeta, which only runs for classes that use it.

Why: @abstractmethod just sets an attribute (__isabstractmethod__ = True) on the function — it is ABCMeta, inherited via ABC, that actually scans a class for any method still carrying that flag and blocks instantiation. Without ABC in the base classes, the decorator is inert.

What abc actually exports

What abc actually exports
NameWhat it is for
ABCa ready-made base class using ABCMeta — inherit from this, not ABCMeta directly
ABCMetathe metaclass that enforces abstract methods — abc.ABC is built from it
abstractmethoddecorator marking a method every concrete subclass must override
abstractpropertydeprecated — use @property with @abstractmethod stacked instead

Together

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

print(Shape.__bases__)
print(type(Shape))

Remember: ABC (not ABCMeta) is what actually enforces @abstractmethod — inheriting from ABC is what makes an unimplemented abstract method block instantiation.

See also: abstract base classes · abstraction · inheritance

ABC

corebeginner

ABC is a base class from the abc module. Inheriting from it (directly or through another ABC) lets a class use @abstractmethod to require its subclasses to implement certain methods before they can be instantiated.

Think of it as

An ABC is a job posting that lists required qualifications instead of hiring anyone: class Shape(ABC): with an abstract area() is Python refusing to "hire" (instantiate) any candidate that has not filled in that requirement. A concrete subclass that implements area() is a completed application; Shape itself never has a completed one, so it can never be hired.

python
from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def required(self):
        ...

    def helper(self):            # normal method, inherited as-is
        return "shared logic"

What we're doing: Define an ABC with two abstract methods and show a subclass stays abstract until every one of them is overridden.

shape.pypython
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

    @abstractmethod
    def perimeter(self):
        ...


class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

    def perimeter(self):
        return self.side * 4


print(Square(4).area())
print(Square(4).perimeter())
print(issubclass(Square, Shape))
3
Shape(ABC) turns on enforcement for both abstract methods declared below.
4
area() is required — any subclass missing it stays abstract.
8
perimeter() is a second, independent requirement — both must be filled in, not just one.
13
Square overrides both, so it is fully concrete and can be instantiated.
Output
16
16
True

Why this works: Square satisfies Shape only because it overrides every abstract method Shape declares — area and perimeter both. Python's ABCMeta tracks the full set of names still carrying __isabstractmethod__ = True through the MRO, and only allows instantiation once that set is empty; a subclass that implemented only one of the two would still raise the same TypeError Shape() itself raises.

Assuming one overridden abstract method is enough when the ABC declares several

Wrong

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

    @abstractmethod
    def perimeter(self):
        ...

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2
    # perimeter never overridden

Square(4)  # still fails

Better

python
class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

    def perimeter(self):        # every abstract method covered
        return self.side * 4

Square(4)  # now concrete

What you see: TypeError: Can't instantiate abstract class Square without an implementation for abstract method 'perimeter'

Why: ABCMeta enforces the entire set of abstract methods, not just the ones a subclass happened to implement — Square inherits perimeter as still-abstract from Shape because nothing in Square overrode it, so the subclass is exactly as uninstantiable as Shape itself.

A subclass must complete every abstract method to become concrete

class Shape(ABC)

area() marked @abstractmethod

Shape()

TypeError — still abstract

class Circle(Shape)

defines area() -> concrete, instantiable

  1. class Shape(ABC) — area() marked @abstractmethod
  2. Shape() — TypeError — still abstract
  3. class Circle(Shape) — defines area() -> concrete, instantiable

What counts as satisfying an ABC

What counts as satisfying an ABC
SituationCan it be instantiated?
Subclass overrides every abstract methodYes — fully concrete
Subclass overrides only some abstract methodsNo — still abstract, same error
Subclass overrides noneNo — identical to instantiating the ABC itself
Unrelated class registered via ABC.register(cls)Yes for isinstance() checks — cls itself is untouched

Together

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

    @abstractmethod
    def perimeter(self):
        ...

class Circle(Shape):
    def area(self):
        return 3.14

# perimeter still missing -> Circle stays abstract
try:
    Circle()
except TypeError as e:
    print(e)

Remember: A subclass of an ABC stays exactly as abstract as the ABC itself until every one of its @abstractmethods is overridden — one out of two is not enough.

See also: abc module · abstractmethod decorator · inheritance

@abstractmethod

standardbeginner

@abstractmethod marks a method as required — it sets a flag (__isabstractmethod__ = True) that ABCMeta checks at instantiation time. The decorator alone does nothing without a base class using ABCMeta (usually via ABC).

Think of it as

@abstractmethod is a sticky note on a form field that says 'must be filled in' — the note itself does not stop anyone from submitting the form; it is the office (ABCMeta) that reads the notes and rejects incomplete submissions. Remove ABC from the class and the sticky notes are still there, but nobody is checking for them.

python
from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def required(self):
        ...              # or a default body a subclass can call via super()

What we're doing: Give an abstract method a default body, and show a subclass can still call it through super() from inside its own override.

logger.pypython
from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message):
        print(f"[base] {message}")   # default body, still required to override


class TimestampLogger(Logger):
    def log(self, message):
        super().log(f"2026-08-21 {message}")   # reuses the default body


TimestampLogger().log("started")
4
@abstractmethod still forces every subclass to override log — even though it has a body here.
5
The body is a usable default, not a placeholder — a subclass's override is free to call it.
10
super().log(...) reaches the abstract method's own body, exactly like overriding any other method.
Output
[base] 2026-08-21 started

Why this works: @abstractmethod's enforcement is entirely about whether a subclass provides its OWN definition of the name — it has nothing to do with whether the abstract method has a body. TimestampLogger.log is still required to exist (and does), but it is free to extend the abstract method's default implementation via super().log(...) rather than reimplementing the printing logic from scratch.

Forgetting @abstractmethod must sit closest to the function when stacked

Wrong

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    @property                # wrong order
    def name(self):
        ...

Better

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @property
    @abstractmethod          # abstractmethod closest to def
    def name(self):
        ...

What you see: AttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable — raised immediately at class-definition time, before Shape is even fully built.

Why: Decorators apply bottom-up, so with @abstractmethod outermost, @property runs first and wraps name in a read-only property object — then @abstractmethod tries to set __isabstractmethod__ = True on that property object and fails, because property does not allow arbitrary attribute assignment. Putting @property outermost instead means @abstractmethod runs first, directly on the plain function, where setting the flag succeeds — then property's own __isabstractmethod__ getter looks for that flag on its wrapped fget and finds it.

Stacking @abstractmethod with other decorators

Stacking @abstractmethod with other decorators
CombinationCorrect order (top to bottom)
Abstract property@property then @abstractmethod
Abstract classmethod@classmethod then @abstractmethod
Abstract staticmethod@staticmethod then @abstractmethod
Plain abstract method@abstractmethod alone

Together

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @property
    @abstractmethod
    def name(self):
        ...

class Circle(Shape):
    @property
    def name(self):
        return "circle"

print(Circle().name)

Remember: @abstractmethod only ever checks that a subclass defines the SAME NAME — never that the override is correct, and never anything beyond instantiation time.

See also: abc module · abstract base classes · static methods

Advertisement

Duck typing and protocols

The structural alternative to abc — compatible by shape, not by declared inheritance.

Python's duck typing

corebeginner

Duck typing means Python calls obj.method() based only on whether obj actually has method — never on obj's declared type or an isinstance() check. Any object with a matching .read() works where a "file-like object" is expected.

Think of it as

Duck typing is a self-checkout scanner, not a bouncer checking ID — it never asks "what class are you," it just tries to scan the barcode (call the method) and either it works or it does not. A PDF reader, a network stream, and a StringIO all get let through the same door as long as each answers .read() correctly.

python
def load(source):
    return source.read()   # works on ANY object with a .read() method

What we're doing: Write one function that accepts three unrelated classes, none sharing a base class, purely because each provides the same method name.

quack.pypython
class Duck:
    def quack(self):
        return "Quack!"


class Person:
    def quack(self):
        return "I'm quacking like a duck!"


class Robot:
    def quack(self):
        return "QUACK.EXE INITIATED"


def make_it_quack(thing):
    return thing.quack()   # no type check — just calls .quack()


for creature in [Duck(), Person(), Robot()]:
    print(make_it_quack(creature))
15
make_it_quack never checks isinstance(thing, Duck) — it just calls thing.quack().
16
If the method exists, the call succeeds — completely independent of thing's class or inheritance.
19
Duck, Person and Robot share no base class at all — duck typing is what still lets one function handle all three.
Output
Quack!
I'm quacking like a duck!
QUACK.EXE INITIATED

Why this works: make_it_quack(thing) works on all three unrelated classes because Python resolves thing.quack() by looking it up on thing at call time, not by checking any declared relationship between the classes. This is the mechanism duck typing describes: "does the object respond to the call I'm about to make," never "is the object officially the right type."

Adding an isinstance() check "to be safe" where duck typing already works

Wrong

python
def make_it_quack(thing):
    if not isinstance(thing, (Duck, Person, Robot)):
        raise TypeError("not a quacker")
    return thing.quack()

class Alien:
    def quack(self):
        return "zorp zorp"

make_it_quack(Alien())  # rejected, even though it works fine

Better

python
def make_it_quack(thing):
    return thing.quack()   # trust the interface, not the class list

class Alien:
    def quack(self):
        return "zorp zorp"

make_it_quack(Alien())  # works — Alien was never anticipated

What you see: A perfectly valid Alien with a working .quack() method gets rejected, purely because nobody added it to the isinstance() tuple.

Why: The isinstance() check reintroduces exactly what duck typing avoids: a closed, maintained list of "acceptable" classes that every new caller must remember to update. Trusting the method call directly (or catching AttributeError if a clearer error message is wanted) keeps the function open to any object that happens to fit, written before or after make_it_quack existed.

Any object that answers the right call passes

FileReader.read()

has the method

NetworkStream.read()

unrelated class, same method

load(source)

calls .read() on either, no type check

  1. FileReader.read() — has the method
  2. NetworkStream.read() — unrelated class, same method
  3. load(source) — calls .read() on either, no type check

Duck typing vs explicit type checking, for the same function

Duck typing vs explicit type checking, for the same function
StyleWhat the function does
Duck typingobj.read() — calls it, trusts obj has it
isinstance() checkif isinstance(obj, SomeType): obj.read() — checks the class first
hasattr() checkif hasattr(obj, "read"): obj.read() — checks the method exists first
try/except (EAFP)try: obj.read() except AttributeError: ... — attempts, handles failure

Together

python
class FileReader:
    def read(self):
        return "file contents"

class NetworkStream:
    def read(self):
        return "stream contents"

def load(source):
    return source.read()   # never checks type — duck typing

print(load(FileReader()))
print(load(NetworkStream()))

Remember: Python calls obj.method() by trying it, never by checking obj's declared type first — any object with the right method works.

See also: protocols · structural typing · polymorphism

Protocols

standardintermediate

typing.Protocol lets you name a shape ("anything with a .read() method") as a real type, so a type checker like mypy can verify duck typing statically. A class satisfies a Protocol just by having the right methods — no inheritance needed.

Think of it as

A Protocol is a job description posted publicly, not a membership card — any candidate whose resume happens to match gets the job, whether or not they ever applied to be considered. class Readable(Protocol): def read(self) -> str: ... describes the shape; any class with a matching read() satisfies it automatically, with zero coordination between the two.

python
from typing import Protocol

class Readable(Protocol):
    def read(self) -> str: ...

What we're doing: Define a Protocol and show two unrelated classes both satisfy it, with no inheritance and no explicit registration.

readable.pypython
from typing import Protocol, runtime_checkable


@runtime_checkable
class Readable(Protocol):
    def read(self) -> str: ...


class TextFile:
    def read(self) -> str:
        return "file contents"


class MemoryBuffer:
    def read(self) -> str:
        return "buffer contents"


def load(source: Readable) -> str:
    return source.read()


for obj in [TextFile(), MemoryBuffer()]:
    print(isinstance(obj, Readable))
    print(load(obj))
3
@runtime_checkable turns on isinstance() support — without it this Protocol is type-checker-only.
4
Readable(Protocol) declares the required shape: a read() method returning str.
19
load(source: Readable) documents the requirement for a type checker — nothing here inherits from Readable.
Output
True
file contents
True
buffer contents

Why this works: TextFile and MemoryBuffer satisfy Readable purely by having a matching read() method — neither inherits from Readable or mentions it anywhere in their own definition. @runtime_checkable is what makes isinstance(obj, Readable) actually inspect obj for the required method names at runtime; without it, Readable would only be meaningful to a static type checker like mypy, not to isinstance().

Expecting isinstance() to work on a Protocol without @runtime_checkable

Wrong

python
from typing import Protocol

class Readable(Protocol):      # no @runtime_checkable
    def read(self) -> str: ...

class MyFile:
    def read(self) -> str:
        return "contents"

isinstance(MyFile(), Readable)  # TypeError!

Better

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Readable(Protocol):
    def read(self) -> str: ...

isinstance(MyFile(), Readable)  # now works

What you see: TypeError: Instance and class checks can only be used with @runtime_checkable protocols

Why: A plain Protocol is designed for static analysis only — checking it at runtime would need to inspect every method by name, which is more expensive and less precise (it cannot check signatures, only presence) than a type checker's static analysis. @runtime_checkable explicitly opts into that runtime presence-only check.

Protocol vs. ABC, for expressing the same requirement

Protocol vs. ABC, for expressing the same requirement
ApproachHow a class satisfies it
abc.ABC + @abstractmethodmust explicitly inherit from the ABC
typing.Protocoljust needs matching methods — no inheritance required
Protocol + @runtime_checkablematching methods AND isinstance() works at runtime
Plain duck typing, no Protocolmatching methods, but nothing for a type checker to verify

Together

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Readable(Protocol):
    def read(self) -> str: ...

class MyFile:
    def read(self) -> str:
        return "contents"

print(isinstance(MyFile(), Readable))

Remember: A Protocol is satisfied just by having matching methods — no inheritance. @runtime_checkable is needed before isinstance() works, and it only checks names.

See also: duck typing · structural typing · abstract base classes

Structural typing

standardintermediate

Structural typing means two types are compatible if they have the same shape (matching methods/attributes) — regardless of name or inheritance. It is the general principle; duck typing is its runtime form, Protocol is its type-checked form.

Think of it as

Structural typing is judging a key by whether it fits the lock, not by which key ring it came from — a UsbCable and a UsbPort do not need to share ancestry, they just need matching shapes at the interface. Nominal typing (Java/C# interfaces) is the opposite: a key must be explicitly cut FOR that lock and labeled as such, even if a differently-labeled key happens to fit perfectly.

python
# structural: no shared base class, still compatible
class A:
    def method(self): ...

class B:
    def method(self): ...   # same shape as A, unrelated

What we're doing: Show the same interface satisfied by inheritance-based (nominal) and shape-based (structural) approaches side by side.

typing_styles.pypython
from abc import ABC, abstractmethod
from typing import Protocol


class Flyer(ABC):                  # nominal: must inherit to count
    @abstractmethod
    def fly(self): ...


class FlyerProtocol(Protocol):     # structural: shape is enough
    def fly(self) -> str: ...


class Airplane(Flyer):             # explicitly declared -> nominal OK
    def fly(self):
        return "flying with engines"


class Bird:                        # never mentions Flyer or FlyerProtocol
    def fly(self):
        return "flying with wings"


print(isinstance(Airplane(), Flyer))         # nominal: declared, so True
print(isinstance(Bird(), Flyer))             # nominal: never declared, False
def send_up(x: FlyerProtocol) -> str:        # structural: shape is enough
    return x.fly()

print(send_up(Bird()))                       # works despite no declaration
4
Flyer(ABC) requires explicit inheritance to satisfy — the nominal approach.
9
FlyerProtocol only requires a matching fly() method — the structural approach, same requirement.
19
Bird never inherits from either Flyer or FlyerProtocol, yet it satisfies the structural one just by having fly().
Output
True
False
flying with wings

Why this works: isinstance(Bird(), Flyer) is False because Flyer is nominal — Bird never declared class Bird(Flyer), so the ABC machinery correctly reports no relationship. send_up(Bird()) still works because it only requires the FlyerProtocol shape (a fly() method), which Bird happens to have — this is the entire distinction: nominal typing checks the declared family tree, structural typing checks the actual shape.

Expecting structural compatibility to satisfy a nominal (ABC) requirement

Wrong

python
from abc import ABC, abstractmethod

class Flyer(ABC):
    @abstractmethod
    def fly(self): ...

class Bird:              # has fly(), but doesn't inherit Flyer
    def fly(self):
        return "flying"

print(isinstance(Bird(), Flyer))  # False, even though Bird "fits"

Better

python
class Bird(Flyer):        # explicitly declare it, satisfying nominal typing
    def fly(self):
        return "flying"

print(isinstance(Bird(), Flyer))  # True

# OR: use a Protocol instead of an ABC, if structural compatibility
# was actually what was wanted all along

What you see: isinstance(Bird(), Flyer) is False, surprising anyone expecting Python to "just work" the way duck typing usually does — because ABC is nominal, not structural.

Why: abc.ABC deliberately opts OUT of Python's usual structural default — it requires an explicit inherits-from relationship, which is what makes @abstractmethod enforcement possible at all. Reaching for a Protocol instead of an ABC is the fix when the actual goal is "any matching shape should work," not "only officially-declared subclasses should work."

Two ways to decide "is this compatible?"

Nominal typing

  • +Must explicitly declare the relationship
  • +class Dog(Animal): or implements Interface
  • +abc.ABC's enforced inheritance is Python's nominal option

Structural typing

  • Judged by shape alone — matching methods
  • No declared relationship needed at all
  • Python's normal duck typing, and typing.Protocol
  • Nominal typing
    • Must explicitly declare the relationship
    • class Dog(Animal): or implements Interface
    • abc.ABC's enforced inheritance is Python's nominal option
  • Structural typing
    • Judged by shape alone — matching methods
    • No declared relationship needed at all
    • Python's normal duck typing, and typing.Protocol

Structural vs. nominal typing, same requirement expressed both ways

Structural vs. nominal typing, same requirement expressed both ways
Typing styleHow compatibility is judged
Structural (Python default / TypeScript)does the object have the right shape?
Nominal (Java, C#, explicit ABC inheritance)was the type explicitly declared to implement this?
Python + typing.Protocolstructural, but visible to a type checker
Python + abc.ABCnominal — must explicitly inherit to count

Together

python
class Duck:
    def quack(self): return "Quack!"

class Person:
    def quack(self): return "I can quack too!"

# neither declares any relationship to the other —
# structural typing is what still lets one function accept both
def make_it_quack(x): return x.quack()

print(make_it_quack(Duck()))
print(make_it_quack(Person()))

Remember: Structural typing judges compatibility by shape (matching methods); nominal typing (abc.ABC) needs an explicit declared relationship instead.

See also: duck typing · protocols · abstract base classes

Advertisement