Python OOP for Java Developers
Source: Dev.to
The Java version
A Square is a Shape. It is defined by its position (x, y) and its side length.

abstract class Shape {
abstract int area();
abstract int perimeter();
}
public class Square extends Shape {
private int x;
private int y;
private int side;
public Square(int x, int y, int side) {
this.x = x;
this.y = y;
this.side = side;
}
public Square(int x1, int y1, int x2, int y2) {
if (Math.abs(x2 - x1) != Math.abs(y2 - y1)) {
throw new IllegalArgumentException("Square must have equal sides");
}
x = x1;
y = y1;
side = Math.abs(x2 - x1);
}
@Override
public int area() {
return side * side;
}
@Override
public int perimeter() {
return 4 * side;
}
@Override
public String toString() {
return String.format("Square[x=%d,y=%d,side=%d]", x, y, side);
}
}
This article is aimed at Java developers, so the Java code is not explained in detail. Feel free to ask questions in the comments.
Translating to Python
Class definition
class Square(Shape):
pass # empty blocks are not allowed in Python, so we’ll fill it later
Constructor (Python has only one __init__ method)
In Python, declaration and initialization happen in the same place, so we declare the fields inside __init__.
def __init__(self, x, y, side):
self.x = x
self.y = y
self.side = side
Creating an instance does not use the new keyword:
s1 = Square(1, 3, 10)
s2 = Square(x=1, y=3, side=10)
Python does not support method overloading, so the second Java constructor (four parameters) cannot be expressed with another
__init__. We’ll implement it with a@classmethodlater.
Encapsulation
Python lacks the public / protected / private keywords. By convention, a leading underscore (_) marks a name as “private”.
The explicit self argument
All instance methods receive the instance as the first parameter, traditionally named self. This is analogous to Java’s this, but self is not a keyword.
def area(self):
return self.side * self.side
def perimeter(self):
return 4 * self.side
String representation
Python uses __str__ and __repr__ instead of Java’s toString.
__str__– user‑friendly representation (used byprintandstr()).__repr__– unambiguous representation, ideally something that could be used to recreate the object.
def __str__(self):
return f"Square [side={self.side}]"
def __repr__(self):
return f"Square(side={self.side})"
sq = Square(5, 0, 5)
print(sq) # Square [side=5]
print(str(sq) + ".") # Square [side=5].
# print(sq + ".") would raise a TypeError because concatenation with a non‑string is not allowed.
| Method | Purpose | Called by |
|---|---|---|
__repr__ | Returns a constructor‑like expression | repr() or the interactive interpreter |
__str__ | Returns a readable string | str(), print() |
Static and class methods
In Java, static methods belong to the class. Python distinguishes static methods (@staticmethod) from class methods (@classmethod).
Static method example
public static double calcSideLength(double area) {
return Math.sqrt(area);
}
import math
@staticmethod
def calc_side_length(area):
return math.sqrt(area) # `math` is a module, not a class
A static method receives no self or cls argument because it does not depend on an instance or the class.
Class method for the second constructor
A class method receives the class itself (cls) as its first argument, making it perfect for factory methods.
@classmethod
def from_points(cls, x1, y1, x2, y2):
"""Create a Square from two opposite corners."""
if abs(x2 - x1) != abs(y2 - y1):
raise ValueError("The points do not form a square")
side = abs(x2 - x1)
# Use the lower‑left corner as the origin (x1, y1)
return cls(x1, y1, side)
sq = Square.from_points(0, 0, 3, 3) # creates a 3×3 square
Exceptions
Java’s throw becomes Python’s raise. The from_points method above already demonstrates raising a ValueError when the supplied points do not form a square.
Full Python implementation
import math
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Square(Shape):
def __init__(self, x, y, side):
self.x = x
self.y = y
self.side = side
@classmethod
def from_points(cls, x1, y1, x2, y2):
"""Factory method that builds a Square from two opposite corners."""
if abs(x2 - x1) != abs(y2 - y1):
raise ValueError("The points do not form a square")
side = abs(x2 - x1)
return cls(x1, y1, side)
@staticmethod
def calc_side_length(area):
"""Return the side length for a given area."""
return math.sqrt(area)
def area(self):
return self.side * self.side
def perimeter(self):
return 4 * self.side
def __str__(self):
return f"Square [side={self.side}]"
def __repr__(self):
return f"Square(side={self.side})"
You can now use the class just like the Java version, but with Pythonic idioms:
sq1 = Square(1, 2, 5)
sq2 = Square.from_points(0, 0, 5, 5)
print(sq1) # Square [side=5]
print(repr(sq2)) # Square(side=5)
print(Square.calc_side_length(25)) # 5.0
Happy coding! If anything is unclear, leave a comment and we’ll clarify.
Using Square.make_square_from_points
try:
sq = Square.make_square_from_points(4, 5, 7, 11)
except ValueError as ve:
print(ve)
Output
ValueError: (4, 5, 7, 11)

Creating a Custom Exception
We’ll create our own error MalformedSquareException by inheriting from ValueError.
class MalformedSquareException(ValueError):
def __init__(self, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
# Note the difference wrt __str__
def __repr__(self):
return "MalformedSquareException(%d, %d, %d, %d)" % (
self.x1, self.y1, self.x2, self.y2
)
def __str__(self):
return (
"The points (%d, %d) and (%d, %d) are not the opposite corners of a square"
% (self.x1, self.y1, self.x2, self.y2)
)
try:
sq = Square.make_square_from_points(4, 7, 5, 11)
except MalformedSquareException as ve:
print(ve)
Output
The points (4, 5) and (7, 11) are not the opposite corners of a square
Abstract Classes
The Shape abstract base class can be written in Python as follows:
import abc
# alternative: class Shape(abc.ABC)
class Shape(metaclass=abc.ABCMeta):
@abc.abstractmethod
def area(self):
... # Python abstract methods require a body
@abc.abstractmethod
def perimeter(self):
...
Python abstract classes are created either by:
- Declaring
ABCMetaas a metaclass, or - Subclassing
ABC(whose metaclass isABCMeta).
As in Java, Python abstract classes can have constructors and non‑abstract methods. They can also define static and class abstract methods.
Example: Static Abstract Method
class Shape(metaclass=abc.ABCMeta):
@staticmethod
@abc.abstractmethod
def nb_sides():
...
class Square(Shape):
@staticmethod
def nb_sides():
return 4
Note:
@staticmethodmust precede@abc.abstractmethod.
Operator Overloading
Python supports operator overloading via special methods such as __add__, __mul__, __lt__, __gt__, etc. In contrast, Java only overloads + for string concatenation.
import abc
import math
class Square(metaclass=abc.ABCMeta):
# sq2 = sq1 * 3 creates sq2 that's √3 times bigger than sq1
def __mul__(self, n):
return Square(self.x, self.y, self.side * math.sqrt(n))
def __lt__(self, other):
return self.side < other.side
s1 = Square(0, 0, 10)
s2 = s1 * 3 # calls __mul__
print(s1 < s2) # calls __lt__
Multiple Inheritance
Python supports multiple inheritance, unlike Java which allows only single inheritance. When a class has multiple ancestors, the resolution order is:
- Immediate ancestors have higher priority than those farther up.
- Within ancestors of the same hierarchy level, the order in the class definition determines priority (left → higher, right → lower).
Example Diagram

Code
class Rectangle(Shape):
def __init__(self, x, y, length, breadth):
self.x, self.y = x, y
self.length, self.breadth = length, breadth
def area(self):
return self.length * self.breadth
def perimeter(self):
return 2 * (self.length + self.breadth)
@staticmethod
def nb_sides():
return 4
class Rhombus(Shape):
def __init__(self, x, y, side, angle):
self.x, self.y = x, y
self.side, self.angle = side, angle
def area(self):
return self.side * self.side * math.sin(self.angle * math.pi / 180)
def perimeter(self):
return 4 * self.side
@staticmethod
@abc.abstractmethod
def nb_sides():
return 4
class Square(Rhombus, Rectangle):
def __init__(self, side):
# Rhombus expects (x, y, side, angle); we set x=y=0 and angle=90°
super().__init__(0, 0, side, 90)
def area(self):
return super().area()
@staticmethod
def nb_sides():
return 4
Key points to note:
Squareinherits from bothRhombusandRectangle.- The method resolution order (MRO) follows the rules described above.
- The
super()call inSquare.__init__correctly forwards arguments toRhombus.__init__(the first parent in the MRO).
Understanding super() and Constructor Calls in Python
- The
super()call does not invoke the parent constructor automatically; it merely provides a reference to the parent object. Therefore, you must call__init__explicitly if you need the parent’s initialization logic. - Because the declaration of
SquarelistsRhombusbeforeRectangle,super()will resolve toRhombusfirst.