A hands-on project exploring how to build your own fully-featured datatypes in Python from scratch, using Object-Oriented Programming (OOP) and magic (dunder) methods to make custom types behave exactly like built-in Python types.
Python lets you define how your objects behave with operators like +, ==, in, and
built-in functions like abs(), str(), len(). This project has two modules, each
going deeper than the last:
| Module | Concept Level | What It Teaches |
|---|---|---|
fraction.py |
Beginner → Intermediate | A single class, arithmetic & comparison operators, auto-simplification |
geometry2d.py |
Intermediate → Advanced | Multiple classes, composition, @property, @classmethod, __contains__, __matmul__ |
Fraction-Custom_Datatype/
│
├── fraction.py # A custom Fraction number type
├── geometry2d.py # 2-D geometry types: Point, Vector, Line, Circle, Rectangle, Triangle
└── README.md # This file
A complete Fraction number type that behaves like a built-in Python number.
Auto-simplifies on creation (Fraction(10, 20) → 1/2) and keeps the sign
always on the numerator.
| Category | Operations |
|---|---|
| Arithmetic | +, -, *, /, ** |
| Comparison | ==, !=, <, <=, >, >= |
| Unary | -f, abs(f) |
| Conversion | float(f), int(f), str(f), repr(f), bool(f) |
| Reverse ops | 2 + Fraction(1,3) — works with int on the left |
| Utilities | .reciprocal(), .to_mixed() |
from fraction import Fraction
f1 = Fraction(3, 4)
f2 = Fraction(1, 2)
print(f1 + f2) # 5/4
print(f1 * f2) # 3/8
print(f1 > f2) # True
print(2 + f1) # 11/4 ← reverse operation
print(Fraction(7, 3).to_mixed()) # 2 and 1/3
print(Fraction(10, 20)) # 1/2 ← auto-simplified| Method | Triggered By | What It Does |
|---|---|---|
__init__ |
Fraction(3, 4) |
Validates, normalises sign, auto-simplifies with GCD |
__str__ |
print(f) |
Human-readable: "3/4" |
__repr__ |
repr(f) |
Dev-friendly: "Fraction(3, 4)" |
__add__ |
f1 + f2 |
Cross-multiply and add |
__sub__ |
f1 - f2 |
Cross-multiply and subtract |
__mul__ |
f1 * f2 |
Numerator × numerator, denominator × denominator |
__truediv__ |
f1 / f2 |
Multiply by reciprocal |
__pow__ |
f1 ** n |
Raise num and den to the power; flip for negative n |
__radd__ |
2 + f1 |
Reverse add so int-first operations work |
__rsub__ |
2 - f1 |
Reverse subtract |
__rmul__ |
3 * f1 |
Reverse multiply |
__rtruediv__ |
3 / f1 |
Reverse divide |
__neg__ |
-f1 |
Negate numerator |
__abs__ |
abs(f1) |
Absolute value of numerator |
__eq__ |
f1 == f2 |
Cross-multiplication equality check |
__lt__ |
f1 < f2 |
Cross-multiplication comparison |
__float__ |
float(f1) |
num / den as float |
__int__ |
int(f1) |
Truncated integer division |
__bool__ |
if f1: |
False only when numerator is 0 |
Five interacting 2-D geometry datatypes that compose together, showing how real-world complex custom types are built.
Point ──────► used by ──────► Vector (interaction via operators)
│ Line (start, end)
│ Circle (center)
│ Rectangle (origin)
└──────────────────────────► Triangle (p1, p2, p3)
Key concept:
Line,Circle,Rectangle, andTriangleare all built on top ofPoint. This is composition — complex types made from simpler ones.
from geometry2d import Point, Vector
p1 = Point(3, 4)
p2 = Point(7, 1)
print(p1.distance_to(p2)) # 5.0
print(p1.midpoint(p2)) # (5.0, 2.5)
print(p1.reflect_x()) # (3, -4)
print(p1.rotate(90)) # (-4, 3) ← 90° around origin
print(p1 + Vector(1, 0)) # (4, 4) ← Point + Vector = new Point
print(p2 - p1) # (4, -3) ← Point - Point = Vector| Magic Method | Behaviour |
|---|---|
__add__(Vector) |
Point + Vector → translated Point |
__sub__(Point) |
Point - Point → displacement Vector |
__sub__(Vector) |
Point - Vector → reverse-translated Point |
__eq__ |
Uses math.isclose() for float-safe equality |
from geometry2d import Vector
v1 = Vector(3, 4)
v2 = Vector(1, 0)
print(v1.magnitude) # 5.0
print(v1.normalized()) # (0.6, 0.8)
print(v1 * 3) # (9, 12) ← scalar multiply
print(3 * v1) # (9, 12) ← reverse scalar multiply
print(v1 @ v2) # 3.0 ← dot product via @
print(v1.angle_between(v2)) # 53.13°
print(v1.cross(v2)) # -4| Magic Method | Behaviour |
|---|---|
__matmul__ |
v1 @ v2 → dot product using the @ operator |
__abs__ |
abs(v) → magnitude |
__neg__ |
-v → reversed direction |
__rmul__ |
3 * v → scalar-first multiply |
magnitude |
@property — computed on access, not stored |
line = Line(Point(0, 0), Point(3, 4))
print(line.length) # 5.0
print(line.midpoint) # (1.5, 2.0)
print(line.slope) # 1.333...
print(line.direction()) # (3, 4) ← a Vector
print(len(line)) # 5 ← __len__ truncates to intc = Circle(Point(0, 0), 5)
print(round(c.area, 2)) # 78.54
print(round(c.circumference, 2)) # 31.42
print(Point(3, 4) in c) # True ← __contains__
print(c.intersects(Circle(Point(8,0), 4))) # True
print(c.scale(2)) # Circle(center=(0, 0), r=10)rect = Rectangle(Point(0, 0), 6, 4)
print(rect.area) # 24
print(rect.center) # (3.0, 2.0)
print(rect.is_square()) # False
print(Point(3, 2) in rect) # True ← __contains__
# Alternate constructor
rect2 = Rectangle.from_corners(Point(0, 0), Point(6, 4))
print(rect == rect2) # Truet = Triangle(Point(0, 0), Point(4, 0), Point(0, 3))
print(t.area) # 6.0
print(t.centroid) # (1.333..., 1.0)
print(t.classify()) # 'right'
print(t.classify_sides()) # 'scalene'
print(Point(1, 1) in t) # True ← __contains__ via barycentric coordinates| Concept | Where Used | What It Means |
|---|---|---|
| Composition | Line, Circle, Rectangle, Triangle |
Complex types built from Point objects |
@property |
Vector.magnitude, Circle.area, Rectangle.center |
Computed attributes accessed like variables, not methods |
@classmethod |
Rectangle.from_corners() |
Alternate constructor — a second clean way to create an object |
__contains__ |
Circle, Rectangle, Triangle |
Powers point in shape with the in keyword |
__matmul__ |
Vector |
Uses the @ operator for dot product — a rare dunder method |
__len__ |
Line |
Powers len(line) |
| Cross-type ops | Point + Vector, Point - Point |
Same operator returns different types based on operands |
| Float safety | All equality checks | math.isclose() instead of == avoids floating-point bugs |
Fraction(1, 0) # ValueError — zero denominator
Fraction(1.5, 3) # TypeError — non-integer inputs
Circle(Point(0,0), -5) # ValueError — negative radius
Line(Point(1,1), Point(1,1)) # ValueError — identical endpoints
Triangle(Point(0,0), Point(1,0), Point(2,0)) # ValueError — collinear points
Rectangle(Point(0,0), 0, 5) # ValueError — zero dimensionEach file has a full demo built in — just run:
python fraction.py
python geometry2d.pyNo installation needed. Both files use only Python's standard math module.
- Start with
fraction.py— one class, clear arithmetic logic, easy to follow. - Move to
geometry2d.py, reading top to bottom:Point→Vector→Line→Circle→Rectangle→Triangle. - Focus on cross-type interactions — how
Point + VectorandPoint - Pointreturn different types from the same operator. - Study
__contains__inCircleandTriangle— the math (distance formula, barycentric coordinates) is as interesting as the Python. - Notice
@propertyvs method — ask yourself: when should something be a property versus a regular method?
Open for learning, experimentation, and extension. Feel free to fork and add more shapes!