Complete guide to Python's __get__ method covering descriptors, attribute access, and property implementation.
Last modified April 8, 2025
This comprehensive guide explores Python’s get method, the special method at the heart of descriptors. We’ll cover basic usage, property implementation, method binding, and practical examples.
The get method is part of Python’s descriptor protocol. It is called when accessing an attribute that is a descriptor. Descriptors enable custom attribute access.
Key characteristics: it takes three parameters (self, instance, and owner), returns the attribute value, and is used to implement properties, methods, and other attribute access patterns.
Here’s a simple descriptor showing get in action. It logs attribute access while maintaining normal behavior.
basic_get.py
class LoggedAccess: def init(self, value): self.value = value
def __get__(self, instance, owner):
print(f"Accessing {self.value} from {instance}")
return self.value
class MyClass: attr = LoggedAccess(42)
obj = MyClass() print(obj.attr) # Prints access message and returns 42
This example demonstrates the basic descriptor pattern. When obj.attr is accessed, Python calls LoggedAccess.get with the instance and owner class.
The instance parameter is None when accessed through the class rather than an instance. The owner parameter is the class where the descriptor is defined.
The property built-in is implemented using descriptors. Here’s how to create a property-like descriptor with get.
property_descriptor.py
class MyProperty: def init(self, getter): self.getter = getter
def __get__(self, instance, owner):
if instance is None:
return self
return self.getter(instance)
class Circle: def init(self, radius): self.radius = radius
@MyProperty
def diameter(self):
return self.radius * 2
circle = Circle(5) print(circle.diameter) # 10
This custom property descriptor stores the getter function and calls it when the attribute is accessed. The instance is None check handles class access.
The @MyProperty decorator works similarly to @property, showing how Python’s property system is built on descriptors.
Python uses get to implement method binding. Here’s how to simulate method binding behavior.
method_binding.py
class Method: def init(self, func): self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
from functools import partial
return partial(self.func, instance)
class MyClass: def init(self, value): self.value = value
@Method
def show(self):
print(f"Value: {self.value}")
obj = MyClass(10) obj.show() # Value: 10
This example shows how Python binds methods to instances. When a method is accessed, get returns a partial function with the instance bound as first argument.
The partial function from functools creates a new function with the instance pre-bound, simulating Python’s method binding.
Here’s a practical descriptor that caches computed properties after first access, optimizing performance for expensive calculations.
cached_property.py
class CachedProperty: def init(self, func): self.func = func self.cache_name = f"cache{func.name}"
def __get__(self, instance, owner):
if instance is None:
return self
if not hasattr(instance, self.cache_name):
setattr(instance, self.cache_name, self.func(instance))
return getattr(instance, self.cache_name)
class Data: def init(self, data): self.data = data
@CachedProperty
def processed_data(self):
print("Processing data...")
return [x * 2 for x in self.data]
d = Data([1, 2, 3]) print(d.processed_data) # Processes and prints print(d.processed_data) # Returns cached value
This descriptor stores computed values in the instance’s namespace. Subsequent accesses return the cached value instead of recalculating.
The cache name is generated dynamically to avoid collisions. This pattern is useful for expensive computations that don’t change after first access.
Descriptors can enforce type checking on attribute access. This example ensures an attribute always has a specific type.
type_checking.py
class Typed: def init(self, name, expected_type): self.name = name self.expected_type = expected_type
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected {self.expected_type}")
instance.__dict__[self.name] = value
class Person: name = Typed(“name”, str) age = Typed(“age”, int)
def __init__(self, name, age):
self.name = name
self.age = age
p = Person(“Alice”, 30)
This descriptor stores values in the instance’s dict while enforcing type constraints. The get method retrieves the stored value.
The descriptor maintains the actual data in the instance’s dictionary while controlling access through the descriptor protocol.
Handle instance=None: Always check for class-level access
Store instance data properly: Use instance.dict to avoid infinite recursion
Document descriptor behavior: Clearly explain expected behavior
Consider performance: Descriptors add overhead to attribute access
Use existing tools when possible: Prefer @property for simple cases
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all Python tutorials.