Give a Python class a list as a class attribute, add one item to each of two objects, and both objects report two items. Nothing in the code looks shared, but the list belongs to the class, not to either object. The distinction behind that bug, between what a class stores once and what each object stores for itself, is the most useful thing to understand about classes, and it works the same way in C++, Java and Python.
This guide explains classes as an object-oriented concept, with one class written in all three languages: what a class is, what it contains, how constructors establish valid objects, the difference between instance and static members, and how each language controls access. 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 the three versions of the main example print identical output, captured verbatim. The code is in a GitHub repository whose build repeats these checks on each commit.
The Short Answer
What is a class in OOP? A class is a user-defined type that bundles data (fields) with the code that works on it (methods). It describes what every object of that type will hold and do; objects are the values created from it.
What is the difference between a class and an object? The class is the definition, written once; an object is one instance, created at run time with its own copy of the fields. One Temperature class can produce any number of Temperature objects, each holding a different value.
What is a static (class) member? A field or method that belongs to the class itself rather than to any object. All objects share one copy: a counter declared static in C++ or Java, or as a class attribute in Python, has the same value whichever object you read it through.
What Is a Class?
A class in object-oriented programming is a template that defines a new type: the data each object of that type holds (fields, also called attributes or member variables) and the operations it supports (methods, also called member functions). Creating an object from a class is called instantiation, and every object made from the same class has the same structure but its own values.
The parts of a class have different names in each language:
| Part | C++ | Java | Python |
|---|---|---|---|
| Data of each object | Data member (double celsius_;) | Field (private final double celsius;) | Instance attribute (self._celsius) |
| Operations | Member function | Method | Method (def fahrenheit(self)) |
| Initialization | Constructor (Temperature(double)) | Constructor (Temperature(double)) | Initializer (__init__) |
| Shared by all objects | static member | static field or method | Class attribute, @classmethod, @staticmethod |
| Access control | public, protected, private (enforced by the compiler) | public, protected, package-private, private (enforced) | Naming conventions; __name is renamed, not hidden |
| Text form of an object | Your own function, or operator<< | toString() | __str__ and __repr__ |
A class is not an object, and in C++ and Java it does not exist at run time as a value you can modify. In Python a class is itself an object (of type type), which is why class attributes can be read and changed at run time, and why the shared-list bug below is possible. Objects themselves, with identity, state and lifetime, are covered in the companion article on objects in this series.
One Class in Three Languages
A Temperature class stores a value in degrees Celsius, refuses values below absolute zero, converts to Fahrenheit, offers a second way to create an object from a Fahrenheit value, and counts how many objects were created.
C++:
// temperature.cpp - one class in C++: private state, a validating constructor,
// a computed value, a static factory and a static counter.
#include <initializer_list>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
class Temperature {
public:
explicit Temperature(double celsius) {
if (celsius < -273.15)
throw std::invalid_argument("below absolute zero: " + format(celsius) + " C");
celsius_ = celsius;
++created_; // counts objects, not calls
}
static Temperature from_fahrenheit(double f) { return Temperature((f - 32) * 5 / 9); }
static int created() { return created_; }
double celsius() const { return celsius_; }
double fahrenheit() const { return celsius_ * 9 / 5 + 32; }
std::string to_string() const {
return format(celsius()) + " C = " + format(fahrenheit()) + " F";
}
private:
static std::string format(double v) {
std::ostringstream out;
out << std::fixed << std::setprecision(1) << v;
return out.str();
}
double celsius_ = 0; // one per object
static inline int created_ = 0; // one per class (C++17)
};
int main() {
Temperature room(21.5);
Temperature boiling(100);
Temperature cold = Temperature::from_fahrenheit(-40);
for (const Temperature* t : {&room, &boiling, &cold})
std::cout << t->to_string() << '\n';
std::cout << "objects created: " << Temperature::created() << '\n';
try {
Temperature impossible(-300);
} catch (const std::invalid_argument& e) {
std::cout << "error: " << e.what() << '\n';
}
std::cout << "objects created: " << Temperature::created() << '\n';
}
Java:
// Temperature.java - the same class in Java.
import java.util.List;
import java.util.Locale;
public class Temperature {
private static int created = 0; // one per class
private final double celsius; // one per object
public Temperature(double celsius) {
if (celsius < -273.15)
throw new IllegalArgumentException("below absolute zero: " + format(celsius) + " C");
this.celsius = celsius;
created++;
}
public static Temperature fromFahrenheit(double f) { return new Temperature((f - 32) * 5 / 9); }
public static int created() { return created; }
public double celsius() { return celsius; }
public double fahrenheit() { return celsius * 9 / 5 + 32; }
@Override
public String toString() { return format(celsius()) + " C = " + format(fahrenheit()) + " F"; }
private static String format(double v) { return String.format(Locale.ROOT, "%.1f", v); }
public static void main(String[] args) {
Temperature room = new Temperature(21.5);
Temperature boiling = new Temperature(100);
Temperature cold = Temperature.fromFahrenheit(-40);
for (Temperature t : List.of(room, boiling, cold))
System.out.println(t);
System.out.println("objects created: " + Temperature.created());
try {
new Temperature(-300);
} catch (IllegalArgumentException e) {
System.out.println("error: " + e.getMessage());
}
System.out.println("objects created: " + Temperature.created());
}
}
Python:
"""temperature.py - the same class in Python."""
class Temperature:
created = 0 # class attribute: one per class
def __init__(self, celsius):
if celsius < -273.15:
raise ValueError(f"below absolute zero: {celsius:.1f} C")
self._celsius = celsius # instance attribute: one per object
Temperature.created += 1
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5 / 9)
@property
def celsius(self):
return self._celsius
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
def __str__(self):
return f"{self.celsius:.1f} C = {self.fahrenheit:.1f} F"
if __name__ == "__main__":
room = Temperature(21.5)
boiling = Temperature(100)
cold = Temperature.from_fahrenheit(-40)
for t in (room, boiling, cold):
print(t)
print("objects created:", Temperature.created)
try:
Temperature(-300)
except ValueError as e:
print("error:", e)
print("objects created:", Temperature.created)
Output (identical for all three):
21.5 C = 70.7 F
100.0 C = 212.0 F
-40.0 C = -40.0 F
objects created: 3
error: below absolute zero: -300.0 C
objects created: 3
The three versions make the same five design decisions:
- The data is private.
celsius_(C++) andcelsius(Java) areprivate, and Python’s_celsiusis private by convention. Callers read it throughcelsius()or thecelsiusproperty and cannot set it at all, so aTemperaturecannot be changed into an impossible one after it is built. - The constructor establishes a valid object. It checks the value first and throws (
std::invalid_argument,IllegalArgumentException,ValueError) if it is below −273.15. The last two lines of the output show the result: the-300object was never created, and the counter stayed at 3. - Fahrenheit is computed, not stored. Each object holds one number;
fahrenheit()derives the other. Storing both would let them disagree. from_fahrenheitis a static (class) method. It needs no existing object, so it belongs to the class. In C++ and Java it isstatic; in Python it is a@classmethod, which receives the class asclsand therefore also works for subclasses.createdis per class, not per object. It isstaticin C++ and Java and a class attribute in Python. The counter is incremented after validation, so a failed construction does not count.
The C++ class here keeps everything in one file; in a larger program the declaration goes in a header and the member function definitions in a .cpp file. C++-specific details such as initializer lists, const member functions and the rules for compiler-generated constructors are in C++ classes explained and constructors and destructors in C++.
Constructors: Making Every Object Valid
A constructor runs once, when an object is created, and its job is to leave the object in a valid state. If it cannot, it should fail (throw an exception) rather than produce an object that breaks the class’s rules. That is what turns a class from a group of fields into a type with guarantees: every Temperature that exists is at or above absolute zero, so no method needs to check again.
Each language has a different default when a class declares no constructor:
| Situation | C++ | Java | Python |
|---|---|---|---|
| No constructor declared | Compiler supplies a default constructor (members of built-in type are left uninitialized unless they have initializers) | Compiler supplies a no-argument constructor; fields get default values (0, null, false) | object.__init__ is used; the object starts with no instance attributes |
| Several ways to construct | Overloaded constructors, default arguments | Overloaded constructors | One __init__; alternatives as @classmethod factories |
| Named alternative constructors | Static member functions | Static methods | @classmethod |
Python’s __init__ is strictly an initializer: the object already exists when it runs (it is created by __new__). In everyday code the difference rarely matters, and __init__ plays the role a constructor plays in C++ and Java.
Instance Members and Static Members
Every field is either stored in each object (an instance member) or stored once for the whole class (a static or class member). In C++ and Java the keyword static makes the choice explicit. In Python the choice depends on where you assign: in a method through self for an instance attribute, or in the class body for a class attribute. That second rule is where the bug from the introduction comes from:
"""class_attribute_pitfall.py - a mutable class attribute is shared by every
object; the fix is to create the list in __init__."""
class SharedCart:
items = [] # one list, stored on the class
def add(self, item):
self.items.append(item) # finds the class's list and changes it
class Cart:
def __init__(self):
self.items = [] # a new list for each object
def add(self, item):
self.items.append(item)
a, b = SharedCart(), SharedCart()
a.add("book")
b.add("lamp")
print("SharedCart:", a.items, b.items, "same list:", a.items is b.items)
c, d = Cart(), Cart()
c.add("book")
d.add("lamp")
print("Cart: ", c.items, d.items, "same list:", c.items is d.items)
Output:
SharedCart: ['book', 'lamp'] ['book', 'lamp'] same list: True
Cart: ['book'] ['lamp'] same list: False
SharedCart.items is created once, when the class body runs. self.items.append(...) looks up items, does not find it on the object, finds the class’s list, and changes it, so both carts see both items and a.items is b.items is True. Cart creates a new list in __init__ for each object. The Python tutorial shows the same mistake with a list of tricks shared by every dog. Assigning (self.items = [...]) would have created an instance attribute instead, which is why the bug appears only with mutable values that are modified in place.
In C++ and Java the equivalent mistake requires writing static, so it is rarer, but the behavior is the same: one copy for the class (Java tutorial: Understanding Class Members, cppreference: static members).
| Use | Instance member | Static / class member |
|---|---|---|
| Stored | Once per object | Once per class |
| Accessed through | An object: t.celsius() | The class: Temperature::created(), Temperature.created |
| Can use instance data | Yes | No (no this or self) |
| Typical uses | The object’s state and behavior | Counters, caches, constants, factory methods |
Access Control: public, private and Python’s Conventions
C++ and Java enforce access at compile time: code outside the class that touches a private member does not compile. Python enforces nothing; it relies on naming conventions, plus one renaming rule:
"""name_mangling.py - Python has no private keyword. A leading underscore is a
convention; two leading underscores rename the attribute, which hides it from
accidental use but not from deliberate access."""
class Account:
def __init__(self):
self._balance = 100 # "internal": a convention only
self.__pin = 1234 # stored as _Account__pin
acct = Account()
print("acct._balance =", acct._balance)
try:
print(acct.__pin)
except AttributeError as e:
print("AttributeError:", e)
print("acct._Account__pin =", acct._Account__pin)
print("attributes:", sorted(vars(acct)))
Output:
acct._balance = 100
AttributeError: 'Account' object has no attribute '__pin'
acct._Account__pin = 1234
attributes: ['_Account__pin', '_balance']
_balance(one underscore) is an ordinary attribute. The underscore tells other programmers it is internal, and tools such as linters respect that, but nothing stops access.__pin(two leading underscores) is renamed to_Account__pin, asvars(acct)shows.acct.__pinraisesAttributeError, butacct._Account__pinworks. The renaming exists to prevent accidental name clashes in subclasses, not to provide security (Python tutorial: Private Variables).
| Level | C++ | Java | Python |
|---|---|---|---|
| Anyone | public | public | no prefix |
| The class and its subclasses | protected | protected (also the same package) | _name by convention |
| The class only | private | private | __name (renamed, still reachable) |
| Default if unspecified | private in a class, public in a struct | Package-private | Public |
Why classes hide their data at all, and how to decide what to expose, is the subject of encapsulation; the C++ treatment is in encapsulation in C++.
Designing a Good Class
The syntax of a class takes minutes to learn; deciding what belongs in one takes longer. These checks catch most design problems:
- One responsibility. A class should have one reason to change.
Temperatureconverts and validates temperatures; it does not format reports or read sensors. - A rule it protects. If every field could take any value and every method is a getter or setter, the class is a record, and a
struct,recordor@dataclasssays so more honestly. - Valid from the first line. The constructor rejects bad input, so no method has to check again.
- The smallest public interface that works. Every public method is a promise to callers.
Temperatureexposes four operations and keeps formatting private. - Derived values are computed. Fahrenheit is calculated from Celsius rather than stored next to it.
A class that passes these checks is also a good base for inheritance, because subclasses can rely on the rules it enforces.
Key Takeaways
- A class is a type: it defines the fields each object holds and the methods that operate on them. Objects are its instances.
- The same design reads the same in C++, Java and Python; the three
Temperatureclasses printed identical output. - Constructors guarantee validity. The
-300object was rejected, and the counter stayed at 3. - Static members are shared. One
createdcounter served all three objects. - In Python, where you assign decides what is shared. A list in the class body was shared by every cart.
- C++ and Java enforce
private; Python uses conventions.__pinwas renamed to_Account__pin, not hidden.
Frequently Asked Questions
Conclusion
A class is a decision about what belongs together: which data, which operations, and which rules every object of that type will keep. The syntax differs between C++, Java and Python, but the decisions are the same, and so are the mistakes, such as storing something once that should exist per object. Getting those decisions right is what makes the other object-oriented ideas, from encapsulation to inheritance and polymorphism, work on top of it.
More object-oriented programming topics are collected in the OOP section.
Source Code and Tests
All programs on this page are in the oop/classes-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.




