Python Objects Under the Hood: Classes, Instances, Attributes, Methods, Inheritance, and MRO
- Published on
- Authors

- Name
- MD IBRAHIM
- @mdibrahim210
Python Objects Under the Hood: Classes, Instances, Attributes, Methods, Inheritance, and MRO
Python Under the Hood — Series 3
In Series 1, we explored how Python executes code, manages objects in memory, organizes names into namespaces, and resolves names using LEGB.
In Series 2, we built on that model to understand function calls, argument binding, mutability, closures, and decorators.
Now we can use the same mental model to understand Python's object-oriented system.
At the surface, this looks simple:
class User:
def greet(self):
return "Hello"
bob = User()
print(bob.greet())
But underneath, several important mechanisms are working together:
Execute class statement
↓
Create a class namespace
↓
Create a class object
↓
Bind the name User to that class object
↓
Call User()
↓
Create an instance object
↓
Initialize the instance
↓
Resolve bob.greet
↓
Search instance → class → inheritance hierarchy
↓
Descriptor protocol binds the method
↓
bob becomes self
↓
Call the underlying function
A useful mental model for this article is:
Classes are objects, instances are objects, attributes are resolved through layered lookup, functions stored on classes become bound methods through the descriptor protocol, and inheritance follows the Method Resolution Order (MRO).

Figure 1 — A class statement creates a class object, instances hold their own state, and attribute lookup moves through the instance and class hierarchy.
1. Classes Are Objects Too
Consider:
class User:
pass
When Python executes this class statement, it does not merely create a template.
It creates a class object.
Conceptually:
User ───► <class object>
You can inspect it:
print(User)
print(type(User))
A class is itself an object that can:
be assigned to another name
be passed to a function
be stored in a collection
be returned from another function
be inspected
be called
For example:
class User:
pass
Person = User
bob = Person()
Conceptually:
User ────┐
▼
class object
▲
Person ──┘
Both names refer to the same class object.
2. What Happens When Python Executes a class Statement?
Consider:
class User:
species = "human"
def greet(self):
return "Hello"
A useful high-level model is:
1. Python encounters the class statement
2. A namespace is prepared for the class body
3. The class body executes
4. Names such as species and greet are created in that namespace
5. Python builds a class object from that namespace
6. The name User is bound to the resulting class object
During class-body execution, you can imagine a temporary namespace containing:
{
"species": "human",
"greet": <function object>
}
Then Python creates the class object:
User
│
▼
<class User>
│
├── species → "human"
└── greet → <function object>
This connects directly to Series 1:
A class body is executable Python code, and the names created while that code runs become part of the class namespace.
3. A Class Namespace
You can inspect a class namespace using:
print(User.__dict__)
You may see names such as:
__module__
species
greet
__dict__
__weakref__
__doc__
The important conceptual part is:
User namespace
species ───► "human"
greet ───► function object
That function object will become especially important later when we explain bound methods.
4. Creating an Instance
Consider:
bob = User()
User is a class object, and class objects are callable.
Calling:
User()
creates an instance.
Conceptually:
User
│
│ call
▼
new User instance
│
▼
bob
Now:
bob ───► <User instance>
The class object and the instance object are different objects:
User ───► class object
bob ───► instance object
You can verify:
print(type(bob))
print(isinstance(bob, User))
5. __new__() and __init__()
Instance creation is often described as if __init__() creates the object.
That is not quite accurate.
At a high level:
User(...)
↓
__new__()
↓
creates / returns the instance
↓
__init__()
↓
initializes that instance
A simple example:
class User:
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
return instance
def __init__(self, name):
self.name = name
Then:
bob = User("Bob")
Conceptually:
1. Call User
2. User.__new__(User, "Bob")
3. New instance is created
4. User.__init__(instance, "Bob")
5. self.name = "Bob"
6. Instance is returned to caller
7. bob is bound to the instance
For most classes, you override __init__() and let the inherited __new__() handle object creation.
6. What Is self?
Consider:
class User:
def greet(self):
return "Hello"
A common beginner misconception is that self is a special Python keyword.
It is not.
self is a conventional parameter name.
This would technically work:
class User:
def greet(current_object):
return "Hello"
But Python convention strongly prefers:
self
because it communicates that the first parameter refers to the current instance.
7. self Is Bound During a Method Call
Suppose:
bob = User()
bob.greet()
The method was defined as:
def greet(self):
Yet you do not manually write:
bob.greet(bob)
So where does self come from?
Conceptually, this:
bob.greet()
behaves approximately like:
User.greet(bob)
That means:
bob ───► self
inside the function call.
This is one of the central mechanisms of Python's object model.
8. Instance Attributes
Consider:
class User:
def __init__(self, name):
self.name = name
Now:
bob = User("Bob")
alice = User("Alice")
Each instance can have its own attributes.
Conceptually:
bob
│
▼
User instance
{
"name": "Bob"
}
alice
│
▼
User instance
{
"name": "Alice"
}
So:
print(bob.name)
print(alice.name)
produces:
Bob
Alice
9. The Instance __dict__
For ordinary user-defined classes, instance attributes are commonly stored in a per-instance namespace accessible through:
bob.__dict__
For example:
print(bob.__dict__)
may show:
{
"name": "Bob"
}
If we add:
bob.age = 30
the instance namespace can become:
{
"name": "Bob",
"age": 30
}
Conceptually:
bob instance namespace
name ───► "Bob"
age ───► 30
10. Class Attributes vs. Instance Attributes
Consider:
class User:
species = "human"
def __init__(self, name):
self.name = name
Here:
species
belongs to the class namespace.
But:
name
is typically stored on each instance.
Conceptually:
User class
│
└── species → "human"
bob instance
│
└── name → "Bob"
alice instance
│
└── name → "Alice"
Both instances can access:
bob.species
alice.species
even though species is not stored directly in either instance.
That works because Python performs attribute lookup through multiple layers.
11. Attribute Lookup
Suppose:
bob.species
Python needs to resolve the attribute:
species
A simplified mental model is:
bob
↓
instance
↓
look for species on instance
↓ not found
look on User class
↓ found
return "human"
So:
bob.species
can find an attribute defined on the class.
This lookup model is more sophisticated in real Python because descriptors and inheritance participate too, but this is the right starting point.
12. Instance Attributes Can Shadow Class Attributes
Consider:
class User:
role = "member"
bob = User()
alice = User()
Initially:
print(bob.role)
print(alice.role)
both find the class attribute:
member
Now:
bob.role = "admin"
This creates an instance attribute.
Conceptually:
User class
└── role → "member"
bob instance
└── role → "admin"
alice instance
└── no role attribute
Now:
print(bob.role)
print(alice.role)
produces:
admin
member
The instance-level binding shadows the class-level binding for bob.
13. A Dangerous Shared Mutable Class Attribute
Consider:
class User:
permissions = []
Then:
bob = User()
alice = User()
bob.permissions.append("admin")
Now:
print(alice.permissions)
may also show:
["admin"]
Why?
Because both instances resolve the same class-level list object.
Conceptually:
User.permissions
│
▼
shared list
▲
│
bob.permissions
alice.permissions
If each instance needs independent mutable state, initialize it per instance:
class User:
def __init__(self):
self.permissions = []
Now each instance receives its own list.
This is closely related to the mutable-default lesson from Series 2:
Shared mutable objects remain shared until you deliberately create separate objects.
14. Methods Start as Functions in the Class Namespace
Consider:
class User:
def greet(self):
return "Hello"
Inside the class namespace:
greet ───► function object
You can see this directly:
print(User.__dict__["greet"])
Conceptually:
User class namespace
greet ───► <function object>
But:
bob.greet
behaves differently.
It produces a bound method.
15. What Is a Bound Method?
Consider:
bob = User()
Now inspect:
print(User.greet)
print(bob.greet)
Conceptually:
User.greet
↓
function object
bob.greet
↓
bound method
├── function → User.greet
└── instance → bob
The bound method remembers which instance should be supplied as the first argument.
This explains why:
bob.greet()
automatically behaves approximately like:
User.greet(bob)
16. The Descriptor Protocol Is Behind Method Binding
Why does accessing:
bob.greet
turn a function into a bound method?
Because Python functions stored on classes implement the descriptor protocol.
At a simplified level:
bob.greet
↓
find greet on User
↓
greet is a function object
↓
function implements descriptor behavior
↓
bind bob to the function
↓
return bound method
Conceptually:
function.__get__(bob, User)
↓
bound method
This is one of the most important "under the hood" facts about Python methods:
Methods are functions whose descriptor behavior automatically binds an instance when accessed through that instance.
17. Calling a Bound Method
Suppose:
class User:
def greet(self, message):
return f"{message}, {self.name}"
and:
bob = User()
bob.name = "Bob"
Calling:
bob.greet("Hello")
can be understood conceptually as:
1. Resolve bob
2. Look up greet
3. Find function on User class
4. Descriptor binds bob
5. Create bound method
6. Call method with "Hello"
7. Parameters become:
self → bob
message → "Hello"
This connects Series 2 directly to Series 3:
method lookup
↓
bound method
↓
ordinary function call
↓
argument binding
↓
execution frame
18. classmethod
Python can bind methods to the class instead of the instance.
Example:
class User:
count = 0
@classmethod
def describe_class(cls):
return cls.__name__
Call:
User.describe_class()
The first parameter:
cls
receives the class object.
Conceptually:
User.describe_class()
↓
cls → User
This makes class methods useful for:
alternative constructors
class-level configuration
subclass-aware behavior
19. staticmethod
A static method does not receive an automatic instance or class binding.
Example:
class Math:
@staticmethod
def add(a, b):
return a + b
Call:
Math.add(2, 3)
Conceptually:
a → 2
b → 3
No automatic:
self
or:
cls
is inserted.
A static method is essentially a function placed inside a class namespace for organizational purposes.
20. Instance Method vs. Class Method vs. Static Method
A useful comparison:
| Method type | Automatic first argument | Typical use |
| --------------- | ------------------------ | --------------------------------------- |
| Instance method | self → instance | work with instance state |
| Class method | cls → class | work with class/subclass state |
| Static method | none | utility logically associated with class |
Conceptually:
bob.method()
↓
self → bob
User.class_method()
↓
cls → User
User.static_method()
↓
no automatic binding
21. getattr(), setattr(), and hasattr()
Python exposes attribute operations dynamically.
Example:
class User:
pass
bob = User()
Set dynamically:
setattr(bob, "name", "Bob")
Equivalent in spirit to:
bob.name = "Bob"
Read:
print(getattr(bob, "name"))
Check:
print(hasattr(bob, "name"))
Delete:
delattr(bob, "name")
These functions show that attribute names can be manipulated dynamically as strings.
22. __getattribute__() and __getattr__()
Attribute access such as:
bob.name
ultimately goes through:
__getattribute__()
for ordinary objects.
A simplified idea:
bob.name
↓
bob.__getattribute__("name")
__getattribute__() is called for attribute access generally.
__getattr__() is different.
It acts as a fallback when normal lookup fails.
Example:
class User:
def __getattr__(self, name):
return f"{name} does not exist"
Then:
bob = User()
print(bob.unknown)
can return:
unknown does not exist
A useful distinction:
__getattribute__
↓
participates in every normal attribute lookup
__getattr__
↓
fallback when normal lookup cannot find the attribute
Overriding __getattribute__() requires care because almost every attribute access passes through it.
23. Inheritance
Consider:
class Animal:
def speak(self):
return "sound"
class Dog(Animal):
pass
Now:
dog = Dog()
print(dog.speak())
Even though Dog does not define:
speak
Python can find it through inheritance.
Conceptually:
dog instance
↓
Dog
↓
Animal
↓
find speak
This is where attribute lookup expands beyond one class.
24. Overriding Methods
A subclass can define its own implementation:
class Animal:
def speak(self):
return "sound"
class Dog(Animal):
def speak(self):
return "woof"
Now:
dog = Dog()
print(dog.speak())
outputs:
woof
Lookup finds the method on Dog before reaching Animal.
Conceptually:
dog
↓
Dog.speak
↓ found
stop lookup
25. super()
Sometimes a subclass wants to extend parent behavior instead of completely replacing it.
Example:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
When:
dog = Dog("Rex", "Labrador")
runs:
Dog.__init__
↓
super().__init__(name)
↓
Animal.__init__
↓
self.name = name
↓
back to Dog.__init__
↓
self.breed = breed
But super() does not simply mean:
"call my parent."
A more accurate model is:
Continue attribute lookup from the next class in the current Method Resolution Order.
That distinction becomes essential with multiple inheritance.
26. The Method Resolution Order — MRO
Consider:
class A:
pass
class B(A):
pass
class C(B):
pass
Python needs a deterministic order for searching classes.
You can inspect it:
print(C.mro())
Conceptually:
C
↓
B
↓
A
↓
object
So attribute lookup follows:
C → B → A → object
This ordering is the Method Resolution Order.
27. Every Ordinary Class Ultimately Inherits from object
In Python 3:
class User:
pass
implicitly inherits from:
object
So the MRO includes:
User
↓
object
You can inspect:
print(User.mro())
This explains where many built-in object behaviors ultimately come from.
28. Multiple Inheritance
Python allows:
class A:
pass
class B:
pass
class C(A, B):
pass
Now Python needs a consistent order for attribute lookup.
The MRO may look like:
C
↓
A
↓
B
↓
object
You can inspect:
print(C.mro())
Python uses the C3 linearization algorithm to compute a consistent MRO.
You do not need to memorize the algorithm immediately.
The practical rule is:
Always think of inheritance lookup in terms of the MRO, not simply "parent then grandparent."
29. The Diamond Inheritance Problem
Consider:
A
/ \
B C
\ /
D
In code:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
Without a deterministic lookup algorithm, Python could face ambiguity.
The MRO gives one consistent order.
For example:
print(D.mro())
conceptually follows:
D
↓
B
↓
C
↓
A
↓
object
The exact ordering follows C3 linearization rules.
30. Why super() Works with Multiple Inheritance
Consider:
class A:
def run(self):
print("A")
class B(A):
def run(self):
print("B")
super().run()
class C(A):
def run(self):
print("C")
super().run()
class D(B, C):
def run(self):
print("D")
super().run()
Calling:
D().run()
can follow the MRO:
D
↓
B
↓
C
↓
A
↓
object
So super() means:
continue lookup from the next class in the MRO
not:
hard-code a specific parent class
This makes cooperative multiple inheritance possible.
31. Attribute Lookup and MRO Together
Suppose:
obj.some_attribute
A simplified lookup model is:
1. Check relevant descriptor behavior
2. Check instance namespace
3. Check class
4. Follow base classes according to MRO
5. If still missing, possibly use __getattr__
6. Otherwise raise AttributeError
This simplified model is enough for most application-level reasoning.
The exact descriptor precedence matters, however, so we will look at descriptors next.

Figure 2 — Attribute lookup follows the class hierarchy according to the MRO, while descriptors can participate in how attributes and methods are resolved.
32. What Is a Descriptor?
A descriptor is an object that defines one or more methods such as:
__get__()
__set__()
__delete__()
Descriptors allow objects stored on a class to customize attribute access.
A very small example:
class Descriptor:
def __get__(self, instance, owner):
return "value from descriptor"
Use:
class Example:
value = Descriptor()
Then:
obj = Example()
print(obj.value)
calls descriptor behavior rather than simply returning the raw Descriptor object.
Conceptually:
obj.value
↓
find value on Example
↓
value is a descriptor
↓
call descriptor.__get__
↓
return computed result
33. Functions Are Descriptors
This is the mechanism behind method binding.
When a function is stored on a class:
class User:
def greet(self):
return "Hello"
the function object has descriptor behavior.
So:
bob.greet
can conceptually trigger:
User.greet.__get__(bob, User)
and return:
bound method
This is why understanding descriptors makes Python methods much less mysterious.
34. property Is Built on the Descriptor Protocol
Consider:
class User:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
Now:
bob = User("Bob", "Smith")
print(bob.full_name)
Notice:
bob.full_name
not:
bob.full_name()
The property object is a descriptor.
Conceptually:
bob.full_name
↓
find full_name on User
↓
property descriptor
↓
property.__get__(...)
↓
call getter
↓
return computed value
35. Property Setters
Properties can also define controlled assignment:
class User:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("age cannot be negative")
self._age = value
Now:
bob = User(30)
bob.age = 31
does not simply create or replace an ordinary instance attribute named age.
The descriptor intercepts assignment.
Conceptually:
bob.age = 31
↓
find age descriptor
↓
descriptor.__set__
↓
run validation
↓
self._age = 31
Descriptors are therefore one of the foundations of managed attributes in Python.
36. Data Descriptors vs. Non-Data Descriptors
Descriptors can be divided broadly into two categories.
A data descriptor defines:
__set__
or
__delete__
in addition to descriptor access behavior.
A non-data descriptor typically defines only:
__get__
This distinction affects attribute lookup precedence.
For example:
property
is normally a data descriptor.
Ordinary function objects acting as methods are non-data descriptors.
A useful simplified precedence model is:
data descriptor
↓
instance dictionary
↓
non-data descriptor / class attribute
↓
base classes through MRO
↓
__getattr__ fallback
This explains why properties can control attribute access even when instance dictionaries exist.
37. Why Methods Can Be Shadowed on an Instance
Because ordinary methods are based on non-data descriptors, an instance attribute can shadow them.
Example:
class User:
def greet(self):
return "Hello"
bob = User()
bob.greet = "not a method anymore"
Now:
print(bob.greet)
returns:
not a method anymore
The instance attribute can take precedence over the class's non-data descriptor.
This is another practical example of descriptor precedence.
38. __slots__
Ordinary instances frequently use:
__dict__
for dynamic attribute storage.
Python also supports:
__slots__
Example:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
This can change how instance attributes are stored and can reduce per-instance memory overhead in some situations.
It may also prevent arbitrary new attributes unless a __dict__ slot is included.
For example:
p = Point(1, 2)
p.z = 3
may raise:
AttributeError
The important lesson is:
__dict__is common, but it is not the only mechanism Python can use to manage instance attributes.
39. Class Objects Have a Type Too
We established that:
User
is an object.
So what is its type?
Usually:
print(type(User))
returns:
<class 'type'>
Conceptually:
bob
↓ type
User
User
↓ type
type
This gives us:
bob is an instance of User
User is an instance of type
That leads toward metaclasses, which are an advanced topic.
For the core mental model, it is enough to know:
Classes are themselves objects, and
typeis the default metaclass that creates most ordinary Python classes.
40. type() Has Two Important Roles
Most developers first see:
type(obj)
which returns the object's class.
But type can also create class objects dynamically.
For example:
User = type(
"User",
(),
{
"species": "human"
},
)
This is conceptually similar to:
class User:
species = "human"
That makes the class-creation model clearer:
class statement
↓
prepare namespace
↓
execute class body
↓
metaclass builds class object
41. Everything Connects Back to Namespaces
Consider:
class User:
role = "member"
def __init__(self, name):
self.name = name
We now have several namespaces:
Module namespace
↓
User → class object
Class namespace
↓
role
__init__
bob instance namespace
↓
name
Accessing:
bob.role
moves through these object/attribute lookup rules.
Calling:
bob.__init__
also involves method binding through descriptor behavior.
The same core ideas from Series 1 remain present:
objects
names
namespaces
lookup
binding
42. One Worked Example Tying Everything Together
Consider:
class Animal:
kingdom = "animal"
def __init__(self, name):
self.name = name
def describe(self):
return f"{self.name} is an animal"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
@property
def description(self):
return f"{self.name} is a {self.breed}"
def describe(self):
parent_text = super().describe()
return f"{parent_text} and a {self.breed}"
Now:
bob = Dog("Bob", "Labrador")
Let's trace it.
Step 1 — Class Objects Already Exist
Python has created:
Animal ───► class object
Dog ───► class object
Dog's MRO is conceptually:
Dog
↓
Animal
↓
object
Step 2 — Calling Dog(...)
Python calls the class object:
Dog("Bob", "Labrador")
An instance is created.
Then:
Dog.__init__
is called with:
self → new Dog instance
name → "Bob"
breed → "Labrador"
Step 3 — super().__init__
Inside:
Dog.__init__
we call:
super().__init__(name)
super() continues lookup from the next class in the MRO.
So Python finds:
Animal.__init__
which sets:
self.name = "Bob"
Then execution returns to:
Dog.__init__
which sets:
self.breed = "Labrador"
The instance namespace now contains conceptually:
{
"name": "Bob",
"breed": "Labrador"
}
Step 4 — Accessing a Class Attribute
Suppose:
print(bob.kingdom)
kingdom is not in the instance namespace.
Lookup continues:
bob instance
↓ not found
Dog
↓ not found
Animal
↓ found
Result:
animal
Step 5 — Calling an Overridden Method
Now:
bob.describe()
Lookup finds:
Dog.describe
before:
Animal.describe
Because the function is a descriptor, Python creates a bound method:
function → Dog.describe
self → bob
Inside that method:
super().describe()
continues along the MRO and finds:
Animal.describe
Step 6 — Accessing a Property
Now:
bob.description
finds:
description
on the Dog class.
That object is a property descriptor.
Its descriptor behavior calls the getter and returns:
Bob is a Labrador
No explicit parentheses are required.
Step 7 — The Whole Model
This one example combines:
class objects
instance objects
instance namespace
class namespace
inheritance
MRO
super()
bound methods
descriptors
properties
All of them fit into one underlying model:
Python objects expose attributes through lookup rules, and classes control much of that lookup behavior.
43. The Object-Oriented Mental Model
When you see:
bob.greet()
train yourself to think:
1. Resolve bob
↓
instance object
2. Look up attribute "greet"
↓
3. Search using attribute rules
↓
instance / descriptors / class / MRO
4. Find function on class
↓
5. Function descriptor binds bob
↓
6. Produce bound method
↓
7. Call method
↓
8. self → bob
↓
9. Create execution frame
↓
10. Execute function body
When you see:
bob.name
think:
attribute lookup
↓
instance state
↓
class and MRO if needed
When you see:
super().method()
think:
continue lookup from the next class in the MRO
44. Five Rules Worth Memorizing
Rule 1 — A class is an object
class User:
pass
creates a class object and binds it to the name:
User
Rule 2 — Instances and classes have different roles
class
↓
defines shared behavior and class-level state
instance
↓
represents individual runtime state
Rule 3 — Methods are functions plus binding behavior
A function stored on a class can become a bound method when accessed through an instance.
Think:
bob.greet
↓
function + bob
↓
bound method
Rule 4 — Inheritance follows the MRO
Do not think only:
child → parent
Think:
object's class
↓
MRO
↓
ordered lookup path
Rule 5 — Descriptors customize attribute access
Descriptors power important Python features such as:
bound methods
property
classmethod
staticmethod
many framework field systems
They are one of the central mechanisms behind Python's object model.
45. Exercise 1 — Class vs. Instance Attribute
Predict:
class User:
role = "member"
bob = User()
alice = User()
bob.role = "admin"
print(bob.role)
print(alice.role)
print(User.role)
Answer
admin
member
member
bob.role = "admin" creates an instance-level attribute.
So:
bob instance
└── role → "admin"
User class
└── role → "member"
alice has no instance-level role, so lookup reaches the class attribute.
46. Exercise 2 — Shared Mutable Class State
Predict:
class User:
tags = []
bob = User()
alice = User()
bob.tags.append("python")
print(alice.tags)
Answer
["python"]
Neither instance has its own tags attribute.
Both resolve:
User.tags
which refers to the same list object.
If each user needs independent tags:
class User:
def __init__(self):
self.tags = []
47. Exercise 3 — Bound Method
Given:
class User:
def greet(self, message):
return message
bob = User()
Which expression is conceptually closest to:
bob.greet("Hello")
Answer
Approximately:
User.greet(bob, "Hello")
Attribute access:
bob.greet
creates a bound method that already carries the bob instance as the value that will become self.
48. Exercise 4 — MRO
Predict the lookup order:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
Answer
Conceptually:
D
↓
B
↓
C
↓
A
↓
object
You can verify with:
print(D.mro())
Python computes the order using C3 linearization.
49. Exercise 5 — Property
Consider:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def diameter(self):
return self.radius * 2
circle = Circle(5)
print(circle.diameter)
What happens?
Answer
Output:
10
diameter is a property descriptor.
Accessing:
circle.diameter
causes the property getter to execute.
No method-call syntax is required.
50. Final Mental Picture
The most important lesson from this article is that Python's object-oriented system is not separate from the execution model we learned earlier.
It is built from the same ideas.
Series 1
Objects
Names
Namespaces
Lookup
Execution frames
↓
Series 2
Functions
Argument binding
Closures
Decorators
↓
Series 3
Classes
Instances
Attributes
Methods
Descriptors
Inheritance
MRO
The connections are direct:
A class
↓
is an object
A class body
↓
executes code in a namespace
An instance
↓
is another object
An attribute
↓
is resolved through lookup rules
A method
↓
starts as a function object
Method binding
↓
uses the descriptor protocol
Inheritance
↓
extends attribute lookup
super()
↓
continues lookup through the MRO
property
↓
is a descriptor
So the complete mental model becomes:
Python's object system is built from objects, namespaces, attribute lookup, function binding, descriptors, and an ordered inheritance path called the MRO.
Where to Go Next
At this point, the first three articles provide a strong understanding of Python's core runtime architecture:
Series 1
Execution, Memory, Namespaces, LEGB
↓
Series 2
Functions, Arguments, Mutability,
Closures, Decorators
↓
Series 3
Classes, Instances, Attributes,
Methods, Inheritance, MRO
A natural fourth and final article in the core series is:
Python Runtime Under the Hood: Iterators, Generators, Context Managers, and Async
That article can explain how Python manages execution over time:
- the iterator protocol,
iter()andnext(),- how
forloops work internally, - generators and
yield, - suspended execution frames,
yield from,- context managers,
__enter__()and__exit__(),- how
withworks, - exceptions and cleanup,
- coroutines,
async def,await,- event loops at a conceptual level,
- async iterators,
- and async context managers.
That would complete the core progression:
Series 1 → How Python executes and resolves names
Series 2 → How Python calls functions and shares state
Series 3 → How Python objects and classes resolve behavior
Series 4 → How Python pauses, resumes, iterates, and coordinates execution