Back to blog
← View series: python tutorials

~/blog

OOP - Advanced Concepts

Apr 1, 20266 min readBy Mohammed Vasim
PythonProgrammingTutorialBeginner

Introduction

In this tutorial, you'll learn advanced OOP concepts in Python: encapsulation, dunder methods (special methods), property decorators, and class methods. These concepts will help you write more robust and professional Python code.

What You'll Learn

  • Encapsulation (public, protected, private)
  • Property decorators
  • Dunder methods (special methods)
  • Class methods and static methods
  • Operator overloading

Encapsulation

Encapsulation restricts access to certain attributes and methods. In Python, this is achieved using naming conventions:

Access Modifiers

TypeConventionDescription
PublicnameAccessible anywhere
Protected_nameConventionally internal (by convention)
Private__nameName mangling (harder to access)

Public (Default)

python
class Person:
    def __init__(self, name):
        self.name = name  # Public - accessible anywhere

person = Person("Alice")
print(person.name)  # Works fine

Protected (Single Underscore)

python
class Person:
    def __init__(self, name):
        self._name = name  # Protected - internal use
    
    def _internal_method(self):  # Internal method
        return "Internal"

person = Person("Alice")
print(person._name)  # Works but "private" by convention

Private (Double Underscore)

python
class Person:
    def __init__(self, name, ssn):
        self.name = name
        self.__ssn = ssn  # Private - name mangled
    
    def get_ssn(self):
        return self.__ssn  # Access through method

person = Person("Alice", "123-45-6789")
print(person.name)       # Alice
# print(person.__ssn)   # Error! Can't access directly
print(person.get_ssn())  # 123-45-6789

# Name mangling in action
print(person._Person__ssn)  # 123-45-6789 (still accessible!)

Note: Python doesn't truly enforce privacy. The underscore conventions signal "don't touch this" to other developers.

Property Decorators

Properties allow you to control access to attributes:

Getter and Setter

python
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    # Getter
    @property
    def celsius(self):
        return self._celsius
    
    # Setter
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero!")
        self._celsius = value
    
    # Convert to Fahrenheit (read-only property)
    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32

temp = Temperature(25)
print(temp.celsius)      # 25
print(temp.fahrenheit)   # 77.0

temp.celsius = 30        # Use setter
print(temp.celsius)      # 30

# temp.celsius = -300   # Raises ValueError

Read-Only Property

python
class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @property
    def diameter(self):
        return self.radius * 2
    
    @property
    def area(self):
        return 3.14159 * self.radius ** 2
    
    @property
    def circumference(self):
        return 2 * 3.14159 * self.radius

circle = Circle(5)
print(circle.radius)        # 5
print(circle.diameter)      # 10
print(circle.area)          # 78.53975
print(circle.circumference) # 31.4159

Dunder Methods (Special Methods)

Dunder (double underscore) methods are special methods that start and end with __. They let you customize object behavior.

str and repr

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    # Human-readable string
    def __str__(self):
        return f"Person: {self.name}, {self.age}"
    
    # Developer string (unambiguous)
    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

person = Person("Alice", 25)
print(str(person))    # Person: Alice, 25
print(repr(person))   # Person(name='Alice', age=25)

eq, lt, le, etc.

python
class Movie:
    def __init__(self, title, rating):
        self.title = title
        self.rating = rating
    
    def __eq__(self, other):
        return self.rating == other.rating
    
    def __lt__(self, other):
        return self.rating < other.rating
    
    def __le__(self, other):
        return self.rating <= other.rating
    
    def __str__(self):
        return f"{self.title} ({self.rating})"

m1 = Movie("Movie A", 8.5)
m2 = Movie("Movie B", 9.0)
m3 = Movie("Movie C", 8.5)

print(m1 == m3)  # True (same rating)
print(m1 < m2)   # True (8.5 < 9.0)
print(m1 <= m3)  # True (8.5 <= 8.5)

add, sub, etc.

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    def __str__(self):
        return f"({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(1, 5)

print(v1 + v2)   # (3, 8)
print(v1 - v2)   # (1, -2)
print(v1 * 3)    # (6, 9)

len, contains

python
class Team:
    def __init__(self, members):
        self.members = members
    
    def __len__(self):
        return len(self.members)
    
    def __contains__(self, member):
        return member in self.members
    
    def __iter__(self):
        return iter(self.members)

team = Team(["Alice", "Bob", "Charlie"])
print(len(team))              # 3
print("Alice" in team)        # True
print("Dave" in team)         # False

for member in team:
    print(member)

call

python
class Counter:
    def __init__(self):
        self.count = 0
    
    def __call__(self):
        self.count += 1
        return self.count

counter = Counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

Class Methods and Static Methods

Instance Methods

python
class MyClass:
    def instance_method(self):
        return "Instance method", self

Class Methods

python
class MyClass:
    class_variable = 0
    
    @classmethod
    def class_method(cls):
        return f"Class method, class: {cls.class_variable}"
    
    @classmethod
    def create_with_value(cls, value):
        # Can create new instances
        obj = cls()
        obj.class_variable = value
        return obj

print(MyClass.class_method())  # Class method, class: 0

obj = MyClass.create_with_value(100)
print(obj.class_variable)      # 100

Static Methods

python
class MyClass:
    @staticmethod
    def static_method(x, y):
        return x + y

# No need to instantiate
print(MyClass.static_method(3, 5))  # 8

When to Use What

Method TypeUse When
InstanceNeed access to instance (self)
ClassNeed access to class (cls) or create instances
StaticUtility function that doesn't need class/instance

Complete Example: Bank Account

python
class BankAccount:
    # Class variable for interest rate
    interest_rate = 0.02
    
    def __init__(self, account_number, balance=0):
        self.__account_number = account_number
        self.__balance = balance
        self.__transactions = []
    
    # Properties
    @property
    def balance(self):
        return self.__balance
    
    @property
    def account_number(self):
        return self.__account_number
    
    # String representations
    def __str__(self):
        return f"Account {self.__account_number}: ${self.__balance:.2f}"
    
    def __repr__(self):
        return f"BankAccount('{self.__account_number}', {self.__balance})"
    
    # Comparison
    def __eq__(self, other):
        return self.__account_number == other.__account_number
    
    def __lt__(self, other):
        return self.__balance < other.__balance
    
    # Core methods
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.__balance += amount
        self.__transactions.append(f"Deposited: ${amount}")
        return self.__balance
    
    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("Insufficient funds")
        self.__balance -= amount
        self.__transactions.append(f"Withdrew: ${amount}")
        return self.__balance
    
    # Class method
    @classmethod
    def create_premium(cls, account_number):
        return cls(account_number, 1000)  # Start with $1000
    
    @classmethod
    def set_interest_rate(cls, rate):
        cls.interest_rate = rate
    
    # Static method
    @staticmethod
    def validate_account_number(number):
        return len(str(number)) == 10

# Test
account1 = BankAccount("1234567890", 500)
account2 = BankAccount("0987654321", 1000)

print(account1)                    # Account 1234567890: $500.00
print(account1 < account2)         # True

account1.deposit(200)
account1.withdraw(100)
print(account1.balance)            # 600

# Using class method
premium = BankAccount.create_premium("1111111111")
print(premium.balance)             # 1000

Summary

In this tutorial, you learned:

  • ✅ Encapsulation (public, protected, private)
  • ✅ Property decorators (getters, setters, read-only)
  • ✅ Dunder methods (str, repr, eq, add, etc.)
  • ✅ Class methods and static methods
  • ✅ Operator overloading

🧑‍💻 Practice Exercise

Create a Rectangle class that:

  1. Uses private attributes for width and height
  2. Has properties for getting/setting width and height
  3. Has a read-only property for area
  4. Implements str, repr
  5. Implements add to combine two rectangles (areas add)
  6. Implements eq to check if two rectangles have same area
Click to see solution
python
class Rectangle:
    """A class to represent a rectangle"""
    
    def __init__(self, width, height):
        self.__width = width   # Private attribute
        self.__height = height  # Private attribute
    
    # Property for width
    @property
    def width(self):
        return self.__width
    
    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        self.__width = value
    
    # Property for height
    @property
    def height(self):
        return self.__height
    
    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive")
        self.__height = value
    
    # Read-only property for area
    @property
    def area(self):
        return self.__width * self.__height
    
    # String representations
    def __str__(self):
        return f"Rectangle({self.__width}x{self.__height})"
    
    def __repr__(self):
        return f"Rectangle(width={self.__width}, height={self.__height})"
    
    # Add two rectangles (areas add)
    def __add__(self, other):
        new_area = self.area + other.area
        # Create new rectangle with same ratio
        return Rectangle(self.__width + other.__width, self.__height + other.__height)
    
    # Check equality (same area)
    def __eq__(self, other):
        return self.area == other.area
    
    def __lt__(self, other):
        return self.area < other.area

# Test the Rectangle class
print("=== Creating Rectangles ===")
r1 = Rectangle(5, 3)
r2 = Rectangle(4, 4)

print(f"r1: {r1}")           # Rectangle(5x3)
print(f"r2: {r2}")           # Rectangle(4x4)
print(f"r1.area: {r1.area}") # 15
print(f"r2.area: {r2.area}") # 16

print("\n=== Property Tests ===")
r1.width = 6
print(f"r1 after width change: {r1}")  # Rectangle(6x3)
print(f"r1.area: {r1.area}")            # 18

print("\n=== Comparison ===")
print(f"r1 == r2: {r1 == r2}")  # False (18 != 16)
print(f"r1 < r2: {r1 < r2}")    # False

print("\n=== Addition ===")
r3 = r1 + r2
print(f"r1 + r2: {r3}")         # Rectangle(10x7)
print(f"r3.area: {r3.area}")    # 70 (18 + 52)

print("\n=== Repr ===")
print(repr(r1))

Output:

=== Creating Rectangles === r1: Rectangle(5x3) r2: Rectangle(4x4) r1.area: 15 r2.area: 16 === Property Tests === r1 after width change: Rectangle(6x3) r1.area: 18 === Comparison === r1 == r2: False r1 < r2: False === Addition === r1 + r2: Rectangle(10x7) r3.area: 70 === Repr === Rectangle(width=6, height=3)

What's Next

In the next tutorial, we'll learn about Error Handling - how to deal with exceptions gracefully in Python.

Error Handling →

Comments (0)

No comments yet. Be the first to comment!

Leave a comment