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:
| Form | What is hidden | Example |
|---|---|---|
| Procedural abstraction | The steps of an algorithm | Calling sort() without knowing which sorting algorithm it uses |
| Data abstraction | How a value is stored | An account exposes balance(); whether it stores the balance or adds up its history is invisible |
| Abstract type | Which class does the work | greeting() 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.FixedClockis arecord, whose generated accessorhour()implements the interface with no extra code. - Python inherits from
abc.ABCand markshour()with@abstractmethod.
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:
| Language | Statement | Result |
|---|---|---|
| C++ | Clock c; | GCC: cannot declare variable 'c' to be of abstract type 'Clock' |
| Java | Clock c = new Clock(); | Clock is abstract; cannot be instantiated |
| Python 3.12 and 3.13 | Clock() | TypeError: Can't instantiate abstract class Clock without an implementation for abstract method 'hour' |
| Python 3.11 | Clock() | 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:
| Aspect | Abstract class | Interface |
|---|---|---|
| C++ | A class with at least one pure virtual function; can have data members and implemented functions | No keyword; by convention a class with only pure virtual functions and a virtual destructor |
| Java | abstract class; fields, constructors, any access level; a class extends only one | interface; no instance fields, abstract and default methods (Java 8+), plus static and private helpers; a class implements any number |
| Python | A class derived from abc.ABC with @abstractmethod methods; checked when an object is created | typing.Protocol; matched by any class with the right methods, checked by static type checkers |
| Use it for | A family of closely related classes that share code or state | A 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:
- Name the abstraction for what it does.
Clock.hour()says what callers get. A name such asTimeHelper.getLocalHourFromSystem()describes one implementation, and the abstraction is lost. - Keep it small.
Clockhas 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. - 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.
- 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:
| Aspect | Abstraction | Encapsulation |
|---|---|---|
| Question it answers | What does this type offer, regardless of implementation? | Who may change this object’s data, and how? |
| Main tools | Abstract classes, interfaces, protocols | Private fields, methods that check every change |
| Level | Design: which operations exist | Implementation: how one class protects its state |
| In the examples | greeting() accepts any Clock | An 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
Clockat compile time; Python raisesTypeErrorwhen 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
Clockinterface 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
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.




