Abstraction in Object-Oriented Programming: Abstract Classes and Interfaces in C++, Java and Python

How abstract classes, interfaces and Python protocols separate what code does from how, with a testable clock example in C++, Java and Python.

A detailed street map beside a simple transit diagram of the same two routes, showing how an abstraction keeps the essentials and drops the details.

A function that reads the system clock to decide between “Good morning” and “Good evening” is hard to test: its output depends on when the test runs. Change it to accept any object that can report the hour, and the problem disappears. The program passes in the real clock; the test passes in a clock stuck at 8:00. The function never knows which one it has. That is abstraction: code written against what something does, not how it does it.

This guide explains abstraction as an object-oriented concept, with the same example in C++, Java and Python: what abstraction means, how abstract classes and interfaces express it, how Python does it with abstract base classes and protocols, and how to tell a useful abstraction from an unnecessary one. Every program was run for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 (-std=c++17 -Wall -Wextra -pedantic -Werror), OpenJDK 21 (javac -Xlint:all -Werror) and Python 3.11, 3.12 and 3.13 (-W error), and every output below is captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit.

The Short Answer

What is abstraction in OOP? Describing a type by what it does (its operations and their promises) and keeping how it does them out of view. Code that uses the type depends only on that description, so any implementation that keeps the promises can be used in its place.

What is the difference between abstraction and encapsulation? Abstraction decides what the outside world sees; encapsulation protects what it does not see. An interface is an abstraction; private fields guarded by methods are encapsulation. Most well-designed classes use both.

What is the difference between an abstract class and an interface? An abstract class can hold fields and working code alongside the operations it leaves unimplemented, and a class can extend only one. An interface (in Java) states operations only, apart from default methods, and a class can implement many. C++ has no interface keyword; a class with only pure virtual functions plays that role.

What Is Abstraction?

Abstraction in object-oriented programming is the practice of representing something by its essential operations while hiding the details of how those operations are carried out. In code, it usually takes the form of an abstract type, an abstract class or an interface, that declares what operations exist and what they promise, with concrete classes that provide the implementations. Code written against the abstract type works with every implementation.

Abstraction is older than object-oriented programming, and it appears at several levels:

FormWhat is hiddenExample
Procedural abstractionThe steps of an algorithmCalling sort() without knowing which sorting algorithm it uses
Data abstractionHow a value is storedAn account exposes balance(); whether it stores the balance or adds up its history is invisible
Abstract typeWhich class does the workgreeting() takes any Clock, real or fixed

Barbara Liskov and Stephen Zilles described the second form in 1974 as abstract data types: a type defined entirely by the operations that can be performed on it. Object-oriented languages add the third form, in which several classes implement the same abstract type and callers can’t tell them apart. That substitution is also what makes inheritance and polymorphism useful.

One Abstraction in C++, Java and Python

The Clock type below promises one thing: hour() returns the current hour, from 0 to 23. SystemClock keeps the promise by reading the real time; FixedClock keeps it by returning whatever hour it was given. greeting() is written against Clock alone:

C++:

// greeting.cpp - abstraction: greeting() depends on what a clock does
// (report the hour), not on how any particular clock finds it out.
#include <ctime>
#include <iomanip>
#include <iostream>
#include <string>

class Clock {
public:
    virtual ~Clock() = default;
    virtual int hour() const = 0;  // 0 to 23; every concrete clock must say how
};

class SystemClock : public Clock {
public:
    int hour() const override {
        std::time_t now = std::time(nullptr);
        return std::localtime(&now)->tm_hour;
    }
};

class FixedClock : public Clock {
public:
    explicit FixedClock(int hour) : hour_(hour) {}
    int hour() const override { return hour_; }

private:
    int hour_;
};

std::string greeting(const Clock& clock) {
    int h = clock.hour();
    if (h < 12) return "Good morning";
    if (h < 18) return "Good afternoon";
    return "Good evening";
}

int main() {
    for (int h : {8, 14, 21}) {
        FixedClock clock(h);
        std::cout << std::setw(2) << std::setfill('0') << h << ":00 -> " << greeting(clock) << '\n';
    }

    SystemClock now;
    std::string g = greeting(now);
    bool known = g == "Good morning" || g == "Good afternoon" || g == "Good evening";
    std::cout << "system clock: hour in 0..23 = " << std::boolalpha
              << (now.hour() >= 0 && now.hour() <= 23) << ", greeting known = " << known << '\n';

    // Clock c;  // does not compile: Clock is abstract
}

Java:

// Greeting.java - abstraction: greeting() depends on what a clock does
// (report the hour), not on how any particular clock finds it out.
import java.time.LocalTime;
import java.util.List;

public final class Greeting {
    interface Clock {
        int hour();  // 0 to 23; every implementation must say how
    }

    static final class SystemClock implements Clock {
        @Override
        public int hour() { return LocalTime.now().getHour(); }
    }

    record FixedClock(int hour) implements Clock {}  // the record's hour() implements Clock

    static String greeting(Clock clock) {
        int h = clock.hour();
        if (h < 12) return "Good morning";
        if (h < 18) return "Good afternoon";
        return "Good evening";
    }

    public static void main(String[] args) {
        for (int h : new int[] {8, 14, 21}) {
            System.out.printf("%02d:00 -> %s%n", h, greeting(new FixedClock(h)));
        }

        Clock now = new SystemClock();
        String g = greeting(now);
        boolean known = List.of("Good morning", "Good afternoon", "Good evening").contains(g);
        System.out.println("system clock: hour in 0..23 = " + (now.hour() >= 0 && now.hour() <= 23)
                           + ", greeting known = " + known);

        // Clock c = new Clock();  // does not compile: Clock is abstract
    }
}

Python:

"""greeting.py - abstraction: greeting() depends on what a clock does
(report the hour), not on how any particular clock finds it out."""
from abc import ABC, abstractmethod
from datetime import datetime


class Clock(ABC):
    @abstractmethod
    def hour(self) -> int:
        """0 to 23; every concrete clock must say how."""


class SystemClock(Clock):
    def hour(self) -> int:
        return datetime.now().hour


class FixedClock(Clock):
    def __init__(self, hour: int) -> None:
        self._hour = hour

    def hour(self) -> int:
        return self._hour


def greeting(clock: Clock) -> str:
    h = clock.hour()
    if h < 12:
        return "Good morning"
    if h < 18:
        return "Good afternoon"
    return "Good evening"


for h in (8, 14, 21):
    print(f"{h:02d}:00 -> {greeting(FixedClock(h))}")

now = SystemClock()
known = greeting(now) in ("Good morning", "Good afternoon", "Good evening")
print(f"system clock: hour in 0..23 = {str(0 <= now.hour() <= 23).lower()}, greeting known = {str(known).lower()}")

# Clock()  # raises TypeError: Clock is abstract

Output (identical for all three):

08:00 -> Good morning
14:00 -> Good afternoon
21:00 -> Good evening
system clock: hour in 0..23 = true, greeting known = true

The first three lines are the reason the abstraction exists. greeting() was checked at 8:00, 14:00 and 21:00 in one run, and the result is the same every time, which is what an automated test needs. The last line uses the real clock; its exact greeting depends on when the program runs, so the program prints only that the hour was in range and the greeting was one of the three. Neither line needed any change to greeting().

Each language declares the abstraction differently:

  • C++ makes hour() a pure virtual function (= 0). A class with at least one pure virtual function is abstract, and each derived class must override it before objects of that class can exist. The details, including pure virtual destructors, are in pure virtual functions in C++.
  • Java uses an interface. FixedClock is a record, whose generated accessor hour() implements the interface with no extra code.
  • Python inherits from abc.ABC and marks hour() with @abstractmethod.
Abstraction: greeting() depends only on what a Clock does The classes in greeting.cpp, Greeting.java and greeting.py greeting(clock) the code that uses the abstraction calls hour() Clock abstract: no objects of its own hour() -> 0 to 23 SystemClock reads the real time used when the program runs FixedClock(8) returns the hour it was given used to test greeting() implement greeting() never names SystemClock or FixedClock. A new clock needs no change to it. With FixedClock(8): 08:00 -> Good morning every run C++ virtual int hour() const = 0; Java interface Clock { int hour(); } Python class Clock(ABC): with @abstractmethod on hour()
greeting() is written against Clock, so any clock will do. A program can pass the system clock and a test can pass a fixed one, with no change to greeting(); Clock itself can’t be instantiated in any of the three languages.

Abstract Types Can’t Be Created

An abstract type describes objects but has none of its own, and all three languages refuse to create one. C++ and Java refuse at compile time; Python refuses when the object is created:

LanguageStatementResult
C++Clock c;GCC: cannot declare variable 'c' to be of abstract type 'Clock'
JavaClock c = new Clock();Clock is abstract; cannot be instantiated
Python 3.12 and 3.13Clock()TypeError: Can't instantiate abstract class Clock without an implementation for abstract method 'hour'
Python 3.11Clock()TypeError: Can't instantiate abstract class Clock with abstract method hour

The same check catches a subclass that forgets an operation: in C++, a class derived from Clock without an hour() override is abstract too, and declaring one produced the same GCC error. Python’s wording changed in 3.12, which is why the Python program in the next section prints its own message rather than the interpreter’s.

Abstract Classes vs Interfaces

Both declare operations without (fully) implementing them. They differ in what else they can contain and in how many a class can inherit:

AspectAbstract classInterface
C++A class with at least one pure virtual function; can have data members and implemented functionsNo keyword; by convention a class with only pure virtual functions and a virtual destructor
Javaabstract class; fields, constructors, any access level; a class extends only oneinterface; no instance fields, abstract and default methods (Java 8+), plus static and private helpers; a class implements any number
PythonA class derived from abc.ABC with @abstractmethod methods; checked when an object is createdtyping.Protocol; matched by any class with the right methods, checked by static type checkers
Use it forA family of closely related classes that share code or stateA capability that unrelated classes can offer

Java’s tutorial puts the choice in the same terms: consider an abstract class to share code among several closely related classes, and an interface when unrelated classes would implement it (Abstract Classes Compared to Interfaces). Clock is an interface by that test: a system clock and a test clock share nothing except the promise.

C++ has a second way to express an abstraction that has no run-time cost: a template accepts any type with the required operations, checked at compile time, and C++20 concepts let the requirements be named and stated. The standard library’s own clocks (std::chrono::system_clock, steady_clock) work that way. Java’s standard library also has an abstract clock, java.time.Clock, with Clock.fixed(...) for tests, which is the same idea as the example above.

Abstraction in Python: ABCs and Protocols

Python offers two ways to state an abstraction, and they check different things. An abstract base class requires implementations to inherit from it, and refuses to create an object that has not implemented every abstract method. A protocol (typing.Protocol, added in Python 3.8) requires only that a class has the right methods. The Python documentation calls this structural subtyping, or “static duck-typing” (typing documentation):

"""protocol.py - two ways to state an abstraction in Python.
An ABC is checked when an object is created; a Protocol is checked only
by a static type checker, and any object with the right method works."""
from abc import ABC, abstractmethod
from typing import Protocol


class Clock(Protocol):
    def hour(self) -> int: ...


class Sundial:  # no base class: it matches Clock by having hour()
    def hour(self) -> int:
        return 9


def greeting(clock: Clock) -> str:
    h = clock.hour()
    if h < 12:
        return "Good morning"
    if h < 18:
        return "Good afternoon"
    return "Good evening"


print(f"Sundial (no base class) -> {greeting(Sundial())}")


class AbstractClock(ABC):
    @abstractmethod
    def hour(self) -> int: ...


class BrokenClock(AbstractClock):  # forgot to implement hour()
    pass


try:
    BrokenClock()
except TypeError:
    print("BrokenClock() -> TypeError: the abstract method hour() was not implemented")

Output:

Sundial (no base class) -> Good morning
BrokenClock() -> TypeError: the abstract method hour() was not implemented

Sundial never mentions Clock, yet greeting() accepted it, because it has an hour() method. A type checker such as mypy would accept it too, since it matches the protocol’s shape. BrokenClock, on the other hand, inherits from an abstract base class and forgot hour(), so creating it failed immediately, before any code could call the missing method. The abc documentation states the rule: a class with abstract methods “cannot be instantiated unless all of its abstract methods and properties are overridden.”

Use an ABC when you want the guarantee enforced at run time and implementations to declare their intent by inheriting. Use a protocol when implementations come from code you don’t control, or when duck typing is enough and you want a type checker to confirm it.

Choosing Good Abstractions

An abstraction is worth having when it lets code ignore a detail it shouldn’t depend on. Four habits help:

  1. Name the abstraction for what it does. Clock.hour() says what callers get. A name such as TimeHelper.getLocalHourFromSystem() describes one implementation, and the abstraction is lost.
  2. Keep it small. Clock has one method, so a new implementation takes a few lines (one, as a Java record). An interface with fifteen methods is hard to implement for a test, and most callers use a few of them.
  3. Put it at a boundary. Time, files, networks, databases and randomness are where abstractions pay for themselves first: they are slow or unpredictable, and tests need to replace them.
  4. Wait for the second implementation. An interface with one implementation and no test double adds a file and a layer of indirection, and its design is often wrong when the second case finally arrives. Extract it when it is needed.

Every abstraction also hides something that can matter. Joel Spolsky called this the Law of Leaky Abstractions: “All non-trivial abstractions, to some degree, are leaky” (Joel on Software, 2002). The Clock above is a small example. SystemClock returns the hour in the computer’s local time zone, which the Clock interface does not mention, so the same program, run on a server set to UTC, greets a user in Tokyo with “Good morning” at 7 p.m. Tokyo time. A better Clock would state its time zone, or take one as a parameter. Good abstractions say which details they hide and which ones the caller still needs to think about.

Abstraction vs Encapsulation

The two are taught together and often confused:

AspectAbstractionEncapsulation
Question it answersWhat does this type offer, regardless of implementation?Who may change this object’s data, and how?
Main toolsAbstract classes, interfaces, protocolsPrivate fields, methods that check every change
LevelDesign: which operations existImplementation: how one class protects its state
In the examplesgreeting() accepts any ClockAn account’s balance changes only through withdraw

They support each other. An abstraction is only as reliable as the implementations behind it, and encapsulation is what keeps those implementations valid. The protection side is covered in encapsulation in object-oriented programming.

Key Takeaways

  • Abstraction describes a type by what it does and hides how. Code written against the abstract type works with every implementation.
  • Abstract types cannot be instantiated. C++ and Java reject Clock at compile time; Python raises TypeError when the object is created.
  • An abstract class can share code and state; an interface states a capability. In C++ both are classes with pure virtual functions; in Python they map to ABCs and protocols.
  • Abstractions make code testable. A fixed clock let one run check three times of day, with the same result on every run.
  • Every non-trivial abstraction leaks somewhere. The system clock’s time zone is a detail the Clock interface does not mention but callers may need.

Conclusion

Abstraction is a decision about what code is allowed to depend on. When greeting() depends on “something that knows the hour” instead of the system clock, it becomes testable, and a new kind of clock costs nothing. C++, Java and Python each have their own syntax for that decision: pure virtual functions, interfaces, and abstract base classes or protocols. In all three, the useful questions are which operations callers really need, and what the abstraction should admit it is hiding.

The concept pages in this series cover the rest of the picture: classes and objects are the building blocks, encapsulation protects their state, and inheritance lets one abstract type have many implementations. More topics are collected in the OOP section.

Source Code and Tests

abstraction-concepts

All programs on this page are in the oop/abstraction-concepts directory of the MYCPLUS C++ examples repository.

Build and test:

bash tests/run_tests.sh g++

What the build checks. On Linux, with GCC and with Clang, it compiles the C++ program with -Wall -Wextra -pedantic -Werror, compiles the Java program with javac -Xlint:all -Werror on Java 21, runs the Python programs with -W error on Python 3.12, and checks that every program prints exactly the output shown on this page. Python 3.11 and 3.13 were checked locally and produced the same output. The compile errors and the TypeError messages in the table were produced by temporarily adding the statements shown; they are not part of the automated build.

Scroll to Top