Python November 01 ,2025

Object-Oriented Programming (OOP) in Python 

Table of Contents

  1. What is Object-Oriented Programming?
  2. Classes and objects —
  3. init, new, lifecycle and del
  4. Instance vs class variables, self and cls
  5. Types of Methods in Python
  6. Encapsulation — Visibility and Name Mangling
  7. Properties, getters/setters, and descriptors
  8. Inheritance — Types, super(), and MRO (Method Resolution Order)
     

1. What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm (a way of designing programs) that organizes code around objects — entities that combine data (attributes) and behavior (methods).

Instead of writing code as a series of functions acting on raw data (as in procedural programming), OOP models real-world entities.

Think of it like this:

  • A car is an object → it has attributes (color, brand, speed) and methods (start, accelerate, brake).
  • A student is an object → it has attributes (name, age, roll_no) and methods (study, attend_exam).

Python implements OOP naturally — everything in Python is an object: integers, strings, lists, functions, and even classes themselves.

Core Concepts of OOP

The four main pillars of OOP are:

  1. Class — The blueprint or template.
  2. Object — The instance of a class.
  3. Encapsulation — Binding data and methods together; controlling access.
  4. Inheritance — Reusing and extending code.
  5. Polymorphism — The same operation behaving differently for different objects.
  6. Abstraction — Hiding complex details and exposing only the essentials.

Let’s break these down one by one with detailed theory and examples.

2. Classes and objects — 

Object-Oriented Programming (OOP) is one of the most powerful paradigms in Python. It allows us to model real-world entities as objects that contain both data (attributes) and behaviors (methods).
In Python, the primary building block of OOP is the class.

a. What Is a Class?

A class is a blueprint or template for creating objects.
It defines the structure and behavior that its objects (instances) will have.

Think of a class like a house blueprint — it describes the layout, materials, and design, but it’s not an actual house. When you use that blueprint to build something, you get a real house — which is an object (or instance) of that class.

In Python, a class groups together:

  • Attributes (Data) → Variables that hold information (like brand, color)
  • Methods (Behavior) → Functions that define actions (like drive(), stop())

Example: Basic Class Definition

class Car:
    wheels = 4  # Class attribute (shared by all instances)

    def __init__(self, brand, color):
        self.brand = brand   # Instance attribute
        self.color = color   # Instance attribute

    def drive(self):
        print(f"{self.color} {self.brand} is driving")

Here:

  • class Car: defines a new class named Car.
  • wheels is a class attribute, shared across all cars.
  • The __init__() method is the constructor, called automatically when creating an object.
  • self refers to the current instance of the class.
  • drive() is an instance method.

b. Creating Objects (Instantiation)

Once a class is defined, you create (or instantiate) objects from it.

An object is an instance of a class — a concrete realization of the class blueprint.

Example: Instantiating the Class

car1 = Car("Tesla", "Red")
car2 = Car("BMW", "Black")

Now:

  • car1 and car2 are two different objects of the Car class.
  • Each has its own data (brand, color).
  • They share the same class attribute wheels.

You can access their data and methods using dot notation:

print(car1.brand)   # Tesla
print(car2.color)   # Black
car1.drive()        # Red Tesla is driving

c. The __init__() Method (Constructor)

When you create an object using Car(...), Python automatically calls the class’s constructor — __init__().

How It Works Internally

  1. When you call Car("Tesla", "Red"):
    • Python first calls Car.__new__() → creates a new empty object.
    • Then it calls Car.__init__() → initializes that object with data.
  2. The self parameter refers to the instance being created.
  3. The values passed to the constructor (brand, color) are used to initialize attributes.

Example:

class Car:
    def __init__(self, brand, color):
        print("Constructor called")
        self.brand = brand
        self.color = color

car = Car("Tesla", "Red")

Output:

Constructor called

d. Understanding self

self is not a keyword — it’s just a naming convention, but it’s crucial.

  • It always refers to the current instance of the class.
  • When you call a method like car1.drive(), Python automatically passes the instance (car1) as the first argument to the method.

So, internally this call:

car1.drive()

is equivalent to:

Car.drive(car1)

This is why every instance method must have self as its first parameter — to access and modify the object’s data.

e. Class Attributes vs Instance Attributes

There are two kinds of attributes in Python classes:

Attribute TypeDefined InsideBelongs ToShared ByExample
Class AttributeClass (outside methods)Class itselfAll instanceswheels = 4
Instance AttributeInside __init__() using selfEach objectUnique per objectself.brand = brand

Example:

class Car:
    wheels = 4

    def __init__(self, brand):
        self.brand = brand

car1 = Car("Tesla")
car2 = Car("BMW")

print(car1.wheels)   # 4
print(car2.wheels)   # 4

Car.wheels = 6       # Change class attribute
print(car1.wheels)   # 6 (affected both)

But instance attributes are unique:

car1.brand = "Audi"
print(car2.brand)  # Still 'BMW'

So, class attributes are shared across all objects, while instance attributes belong to individual instances.

f. What Python Stores Internally

When you define a class, Python creates a class object in memory.
Both the class and each instance maintain their own attribute dictionaries.

Let’s understand this internal mechanism.

Class Attribute Dictionary (__dict__)

Every class in Python maintains a dictionary called __dict__ that stores all of its attributes and methods.

class Car:
    wheels = 4
    def drive(self):
        pass

print(Car.__dict__)

Output (partial):

{'__module__': '__main__', 'wheels': 4, 'drive': , '__dict__': , ...}

Here you can see:

  • wheels (data)
  • drive (function)
  • Special attributes like __module__, __dict__, etc.

Instance Attribute Dictionary (__dict__)

Each object (instance) has its own __dict__, storing instance-specific data.

car1 = Car("Tesla", "Red")
print(car1.__dict__)

Output:

{'brand': 'Tesla', 'color': 'Red'}

So, attributes like brand and color are stored per object.

g. Attribute Lookup Mechanism

When you access an attribute like car1.brand, Python follows this lookup chain:

  1. Check the instance’s __dict__
    • If found, return the value.
  2. If not found, check the class’s __dict__
    • If the class defines it (like wheels), return that.
  3. If still not found, check parent classes
    • According to the Method Resolution Order (MRO).

This process is known as attribute resolution.

Example:

class Car:
    wheels = 4
    def __init__(self, brand):
        self.brand = brand

car = Car("Tesla")
print(car.wheels)   # Looks in instance → not found → class → found (4)

If you assign to car.wheels, Python creates a new instance variable, shadowing the class attribute.

car.wheels = 6
print(car.wheels)   # 6 (instance-level)
print(Car.wheels)   # 4 (class-level)

h. Bound Methods and the Role of Functions in Classes

In Python, when you define a method inside a class, such as def drive(self):, it is not a method yet — it is simply a function object stored in the class’s dictionary. Nothing special happens at definition time. The magic occurs when you access that function through an instance. Python automatically converts the function into a bound method, which means it attaches the instance as the first argument (self) behind the scenes. This binding mechanism is why you don’t need to manually pass the instance when calling: Python injects it for you.

When you access the function directly from the class (e.g., Car.drive), you get an unbound function, because it has not been attached to any specific object. But when you access it from an instance (e.g., car1.drive), Python wraps the function and turns it into a bound method, where the instance is already stored inside this wrapper.

This is why:

  • Car.drive → just a function
  • car1.drive → a bound method that already knows it belongs to car1

Thus, calling car1.drive() is equivalent to calling Car.drive(car1) internally.

Example

class Car:
    def drive(self):
        print("The car is driving.")

car1 = Car()

print(Car.drive)     # function (unbound)  
print(car1.drive)    # bound method  

Output conceptually:


>

This shows that the second one is automatically tied ("bound") to car1.

If you want, I can format this in your OOP blog style with headings + bullets.

i. The Role of type and Metaclasses

In Python, everything is an object — not just numbers or strings, but also functions, modules, and even classes themselves.

This leads to a powerful idea:

Classes are objects.

Classes are created by another class called a metaclass.

Let’s break this down in a simple and intuitive way.

Classes Are Objects

When you write:

class Car:
    wheels = 4

You might think Python simply creates a class in memory.
But what actually happens is:

  • Python executes the class body like normal code
  • Everything inside the class (attributes, methods) is collected into a dictionary
  • Then Python calls a metaclass (by default, type) to create the class object

This means:

Car is an object

Yes — class Car itself is an object.

Check it:

print(type(Car))

Output:

Car

This tells us:

✔️ Car is an object
✔️ That object is created by the built-in metaclass type

Objects vs Classes vs Metaclasses

To visualize this:

car1 (object)   --> instance of -->   Car (class)
Car (class)     --> instance of -->   type (metaclass)

Or in words:

  • car1 is an instance of the class Car
  • Car is an instance of the type metaclass

Example:

car1 = Car()

print(type(car1))   # 
print(type(Car))    # 

So What Is a Metaclass?

A metaclass is the class that creates classes.

If classes create objects…
Metaclasses create classes.

In Python:

  • When you create an object → ClassName.__init__ runs
  • When you create a class → type.__new__ and type.__init__ run

Metaclasses define:

  • how a class behaves
  • how attributes are added
  • how methods are created
  • how inheritance works
  • how you can modify or customize class creation

Why Python Uses Metaclasses

If classes are objects, Python must have something that creates them.

Just like this:

  • You create car1 by calling Car()
  • Python internally creates Car by calling type()

This gives Python its famous flexibility:

✔ Add attributes to classes at runtime

✔ Modify class behavior dynamically

✔ Automatically enforce rules (e.g., in frameworks like Django, SQLAlchemy)

✔ Implement design patterns at class-creation level

Creating a Class Dynamically Using type()

Since type creates classes, you can create a class manually:

Car = type(
    "Car",                 # class name
    (object,),             # base classes
    {"wheels": 4}          # attributes/methods
)

c = Car()
print(c.wheels)

This works exactly like:

class Car:
    wheels = 4

Because both are created by the same metaclass: type.

Custom Metaclasses

You can define your own metaclass by inheriting from type:

class MyMeta(type):
    def __new__(cls, name, bases, attrs):
        print("Creating class:", name)
        return super().__new__(cls, name, bases, attrs)

Use it in a class:

class Car(metaclass=MyMeta):
    pass

Output:

Creating class: Car

Why and When we use custom metaclasses, with real-world-style examples.

First, a simple mental model

  • Class → creates objects
  • Metaclass → creates classes
metaclass  →  class  →  object

So if:

  • decorators modify functions
  • base classes modify instances
  • metaclasses modify classes at creation time

Why do we use custom metaclasses?

1️⃣ Enforce rules on all classes automatically

You can force every class using your framework/library to follow rules.

Example: Require a specific method

class MustHaveStartMethod(type):
    def __new__(cls, name, bases, attrs):
        if "start" not in attrs:
            raise TypeError(f"{name} must define a start() method")
        return super().__new__(cls, name, bases, attrs)
class Engine(metaclass=MustHaveStartMethod):
    def start(self):
        print("Engine started")

❌ This will fail:

class BrokenEngine(metaclass=MustHaveStartMethod):
    pass

Why useful?

  • Framework safety
  • Prevents incorrect class definitions early
  • Errors happen at import time, not runtime

2️⃣ Automatically modify class attributes

Metaclasses can inject or modify attributes for every class.

Example: Auto-register classes (VERY common)

class RegistryMeta(type):
    registry = {}

    def __new__(cls, name, bases, attrs):
        new_class = super().__new__(cls, name, bases, attrs)
        cls.registry[name] = new_class
        return new_class
class Car(metaclass=RegistryMeta):
    pass

class Bike(metaclass=RegistryMeta):
    pass
print(RegistryMeta.registry)

Output:

{'Car': , 'Bike': }

Why useful?

  • Plugin systems
  • ORMs
  • Command registries
  • Django models, serializers

3️⃣ Create DSLs (Domain-Specific Languages)

Metaclasses allow writing clean declarative code.

Example: ORM-like behavior

class ModelMeta(type):
    def __new__(cls, name, bases, attrs):
        fields = {
            key: value
            for key, value in attrs.items()
            if isinstance(value, str)
        }
        attrs["_fields"] = fields
        return super().__new__(cls, name, bases, attrs)
class User(metaclass=ModelMeta):
    name = "text"
    email = "text"
print(User._fields)

Output:

{'name': 'text', 'email': 'text'}

Why useful?

  • Declarative syntax
  • Clean APIs
  • Frameworks like Django, SQLAlchemy use this heavily

4️⃣ Modify inheritance behavior

Metaclasses can change how inheritance works.

Example:

  • Prevent multiple inheritance
  • Automatically mix in methods
  • Track subclasses
class TrackSubclasses(type):
    def __init__(cls, name, bases, attrs):
        cls.subclasses = []
        for base in bases:
            if hasattr(base, "subclasses"):
                base.subclasses.append(cls)
class Animal(metaclass=TrackSubclasses):
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

print(Animal.subclasses)

Output:

[, ]

5️⃣ Framework-level magic (real-world usage)

You rarely write metaclasses in everyday apps, but you use them all the time indirectly:

FrameworkMetaclass Use
DjangoModel creation, field discovery
SQLAlchemyORM mapping
ABC (abc.ABCMeta)Abstract base classes
EnumEnum creation
DataclassesClass transformation

Example you already use:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

👉 ABC uses ABCMeta behind the scenes.

Why Do Frameworks Use Metaclasses?

Metaclasses allow frameworks to:

  • auto-register classes
  • validate attributes
  • add extra methods
  • transform fields (like Django’s ORM models)
  • enforce naming conventions
  • create singletons
  • implement interface enforcement

Examples in real-world Python:

  • Django Models
  • SQLAlchemy ORM
  • Pydantic
  • Dataclasses (internally use similar mechanisms)

Understanding the Chain: Instance → Class → Metaclass

Here’s the complete hierarchy:

object_instance  --->  created by  --->  class
class            --->  created by  --->  metaclass
metaclass        --->  instance of --->  type
type             --->  instance of --->  itself

Yes — type is an instance of type!

print(type(type))   # 

This circular design is what makes Python a consistent, flexible, fully object-oriented language.

Benefits of Metaclasses

  • Customize how classes are built
  • Add automatic behaviors
  • Enforce rules at class creation time
  • Enable powerful frameworks and ORMs

 

3. __init__, __new__, lifecycle and __del__

A) __new__ —

What It Is:

__new__ is the actual constructor in Python.
It is the method that creates (allocates) the object before initialization.

Explanation:

  • Runs before __init__
  • Responsible for creating and returning a new instance
  • First argument is cls (the class)
  • Mostly used when controlling creation (Singletons, immutable types)

Example (Singleton):

class MySingleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

B) __init__ — 

What It Is:

__init__ is the initializer.
It does NOT create the object — it only initializes it after creation.

Explanation:

  • Runs after __new__
  • First argument is self (the object created by __new__)
  • Used to set attributes or run setup code

Example:

class Car:
    def __init__(self, brand):
        self.brand = brand

C) Object Lifecycle — 

What It Is:

The object lifecycle is the complete journey of an object from creation → initialization → usage → destruction.

Lifecycle Steps:

1. __new__()        → object is created in memory
2. __init__()       → object is initialized
3. object is used   → methods, attributes
4. references end
5. garbage collector decides to remove it
6. __del__()        → finalizer (if called)

This is how Python manages object creation and destruction internally.

D) __del__ — 

What It Is:

__del__ is Python’s destructor—a method called when an object is about to be garbage-collected.

Explanation:

  • Runs when Python decides the object is no longer needed
  • Timing is not guaranteed
  • Should NOT be used for important cleanup
  • Prefer:
    • with statement
    • context managers
    • explicit .close()

Example:

class Example:
    def __del__(self):
        print("Object destroyed")

 

4. Instance vs class variables, self and cls

  • Instance variable: self.x — belongs to a single object.
  • Class variable: ClassName.y or cls.y — shared across instances.

Example showing difference:

class A:
    shared = []
    def __init__(self, value):
        self.value = value

a1 = A(1); a2 = A(2)
A.shared.append('x')   # visible from both a1 and a2
a1.value = 10          # only affects a1

Be careful with mutable class attributes (like lists or dicts) — they are shared. Use instance attributes for per-instance state.

5. Types of Methods in Python

In Python, methods are functions defined inside a class that describe the behavior of objects or the class itself.
They allow interaction with data stored in objects or shared at the class level.

There are three main types of methods in Python classes:

  1. Instance Methods
  2. Class Methods
  3. Static Methods

Each has a different purpose, depending on what part of the class they operate on — object instance, entire class, or neither.

1. Instance Methods

Instance methods are the most common type of methods in Python.
They operate on individual objects (instances) of a class and can access or modify their instance variables.

Syntax:

def method_name(self, other_parameters):
    # method body
  • The first parameter is always self, which refers to the current instance of the class.
  • Through self, the method can access instance variables and other methods of that specific object.

Example:

class Student:
    def __init__(self, name, marks):
        self.name = name
        self.marks = marks
    
    def display(self):
        print(f"Name: {self.name}, Marks: {self.marks}")

s1 = Student("Alice", 90)
s1.display()

Output:

Name: Alice, Marks: 90

Explanation:

  • The method display() uses self to access self.name and self.marks — data belonging to that particular object.
  • Instance methods are used when you want to work with data specific to each object.

2. Class Methods

Class methods are methods that work on the class as a whole, not on individual objects.
They are used when you need to access or modify class variables shared among all instances.

Syntax:

@classmethod
def method_name(cls, other_parameters):
    # method body
  • They use the @classmethod decorator.
  • The first parameter is cls, which refers to the class itself, not an instance.
  • Class methods can modify class-level attributes that affect all instances.

Example:

class C:
    count = 0   # class variable

    def __init__(self):
        C.count += 1

    @classmethod
    def get_count(cls):
        return cls.count

obj1 = C()
obj2 = C()
print(C.get_count())

Output:

2

Explanation:

  • Each time an object is created, count increases by 1.
  • The class method get_count() accesses the class variable count using cls.count.
  • Class methods are often used for factory methods or class-wide operations.

3. Static Methods

Static methods are utility functions that belong to a class logically but do not access class or instance data.
They don’t take self or cls as parameters.
They are used when a method performs a general task related to the class but doesn’t need access to its data.

Syntax:

@staticmethod
def method_name(parameters):
    # method body
  • They use the @staticmethod decorator.
  • They behave like normal functions but are grouped inside the class for organization.

Example:

class Math:
    @staticmethod
    def add(a, b):
        return a + b
    
    @staticmethod
    def multiply(a, b):
        return a * b

print(Math.add(5, 3))
print(Math.multiply(4, 2))

Output:

8
8

Explanation:

  • Both methods are independent of the class or instance — they just perform calculations.
  • They are included inside the class because they are logically related to mathematical operations.

Why These Methods Are Needed

✅ To separate instance-specific logic, class-wide behavior, and independent utilities.
✅ To make code more modular, organized, and reusable.
✅ To control how data is accessed and modified — instance vs class level.
✅ To improve readability and maintainability of object-oriented programs.

6. Encapsulation — Visibility and Name Mangling

Encapsulation is one of the core principles of Object-Oriented Programming (OOP).
It means binding data (variables) and methods (functions) that operate on that data into a single unit (a class), while also restricting direct access to some of an object’s internal details.

In simple terms —
Encapsulation allows hiding internal data from outside interference and misuse.
It helps in data protection, code organization, and controlled access through methods.

However, unlike languages such as Java or C++, Python does not have strict access modifiers (public, private, protected).
Instead, it follows naming conventions and name mangling to indicate how data should be accessed.

Types of Visibility in Python

Python uses three visibility levels (based on naming conventions):

TypeSyntaxMeaningAccess
PublicnameCan be accessed freely from anywhereNo restriction
Protected_nameShould be treated as internal (not enforced)Can be accessed, but discouraged
Private__nameName mangling prevents direct accessAccessible only in a special way

A. Public Members

  • Public members are accessible from anywhere — inside or outside the class.
  • They are created without underscores.

Example:

class Example:
    def __init__(self):
        self.name = "Public Variable"

e = Example()
print(e.name)  # Accessible from outside

Output:

Public Variable

Explanation:
The variable name is public, so it can be accessed and modified directly by any code.

B. Protected Members (Convention Only)

  • A single underscore prefix (_) indicates that a variable or method is intended for internal use only.
  • This is just a convention, not enforced by Python — it signals to programmers, “Don’t touch this unless you know what you’re doing.”

Example:

class Example:
    def __init__(self):
        self._protected = "Internal Variable"

e = Example()
print(e._protected)  # Accessible but discouraged

Output:

Internal Variable

⚠️ Explanation:
The variable _protected can still be accessed from outside, but by convention, it should be treated as a non-public part of the class.

Protected members are mostly used when inheriting classes, where subclasses can still access them safely.

C. Private Members (Name Mangling)

  • Private members are defined with a double underscore prefix (__).
  • Python internally performs name mangling — it automatically changes the variable name to _ClassName__variableName.
  • This helps avoid accidental access or name conflicts in subclasses.

Example:

class Example:
    def __init__(self):
        self.__private = "Hidden Variable"

e = Example()
# print(e.__private)  # ❌ Error: Attribute not found
print(e._Example__private)  # ✅ Correct way to access

Output:

Hidden Variable

Explanation:

  • Python rewrites __private internally as _Example__private.
  • This process is called name mangling.
  • It prevents accidental modification of internal variables, especially in inheritance scenarios.

How Name Mangling Works Internally

Let’s check how Python renames the private variable:

class Sample:
    def __init__(self):
        self.__data = 100

s = Sample()
print(dir(s))  # list all attributes

Output (simplified):

['_Sample__data', '__init__', ...]

✅ You can see that Python automatically changed __data → _Sample__data.
This means private attributes are still accessible, but only by knowing their mangled name.

Why Encapsulation and Name Mangling Are Used

  1. Data Hiding: Prevents direct modification of sensitive data.
  2. Code Safety: Reduces accidental changes to internal variables.
  3. Cleaner Interface: Exposes only necessary parts of the class to users.
  4. Avoids Conflicts: Protects internal variables from being overridden in subclasses.
  5. ⚠️ Not for Security: Name mangling only discourages access — it doesn’t make data truly private or secure.

Example Combining All Three

class E:
    def __init__(self):
        self.public = 1
        self._protected = 2
        self.__private = 3

e = E()
print(e.public)           # ✅ Accessible
print(e._protected)       # ⚠️ Works, but not recommended
print(e._E__private)      # ✅ Access through name mangling

Output:

1
2
3

7. Properties, getters/setters, and descriptors

A. Properties

What it is

A property lets you access a method like an attribute, while still running code behind the scenes.

Why it’s used

  • To add validation
  • To compute values
  • To control read/write access
    —without changing the way the attribute is accessed.

Example (property as computed attribute)

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

r = Rectangle(4, 5)
print(r.area)  # 20  (looks like attribute, runs method)

B. Getters and Setters

What they are

  • Getter → method called when reading an attribute
  • Setter → method called when assigning to an attribute

Python uses @property to define getters, and @.setter to define setters.

Why they exist

To add validation or constraints while keeping attribute-like syntax.

Example (validation using setter)

class Person:
    def __init__(self, age):
        self._age = age

    @property
    def age(self):          # getter
        return self._age

    @age.setter
    def age(self, value):   # setter
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

p = Person(25)
p.age = 30     # valid
print(p.age)   # 30

C. Descriptors

What is a Descriptor?

A descriptor is an object that controls how an attribute is accessed on a class.

Any object that defines one or more of these methods is a descriptor:

__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)

When a descriptor is assigned as a class attribute, Python automatically routes
attribute access through these methods.

In simple words:

Descriptors let you intercept attribute access (obj.attr) and decide what happens.

Why Descriptors Matter

Descriptors are foundational in Python. Many “high-level” features are built on them:

  • property
  • Instance methods (def func(self):)
  • @staticmethod
  • @classmethod
  • ORM fields (Django, SQLAlchemy)
  • Validation systems
  • Lazy loading
  • Computed attributes

If you understand descriptors, you understand how Python objects really work.

How Attribute Access Works (Internally)

When Python sees:

obj.attr

It roughly does:

  1. Look for attr in the class
  2. If found and it’s a descriptor, call:

    attr.__get__(obj, type(obj))
    
  3. Otherwise, fall back to instance dictionary

This is why descriptors must be class attributes, not instance attributes.

Simple Descriptor Example (Manual property)

class PositiveNumber:
    def __get__(self, instance, owner):
        return instance._value

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Value must be positive")
        instance._value = value
class Sample:
    number = PositiveNumber()

Usage:

s = Sample()
s.number = 10      # calls __set__
print(s.number)    # calls __get__

What happens step-by-step?

s.number = 10

➡ Python finds number on the class
➡ Sees it has __set__
➡ Calls:

PositiveNumber.__set__(number, s, 10)
print(s.number)

➡ Calls:

PositiveNumber.__get__(number, s, Sample)

How This Relates to property

This:

class Sample:
    @property
    def number(self):
        return self._value

    @number.setter
    def number(self, value):
        if value < 0:
            raise ValueError("Value must be positive")
        self._value = value

Is internally doing the same thing as your descriptor example.

property is simply a pre-built descriptor.

When Should You Use Descriptors?

✅ Use descriptors when:

  • You need reusable attribute logic
  • Multiple attributes share behavior
  • You want framework-level control
  • property becomes repetitive

❌ Avoid descriptors when:

  • Simple getters/setters are enough
  • Readability matters more than control

 

Read more here-  Inheritance — Types, super(), and MRO (Method Resolution Order)

 

Next Blog- Polymorphism — Duck Typing and Interfaces

Sanjiv
0

You must logged in to post comments.

Get In Touch

Kurki bazar Uttar Pradesh

+91-8808946970

techiefreak87@gmail.com