On this article, you’ll study what Python descriptors are and methods to use them to construct three kinds of managed attributes: validated, computed, and lazily loaded.
Subjects we are going to cowl embrace:
- Find out how to construct a validated attribute descriptor that enforces kind and vary constraints on class attributes.
- Find out how to construct a computed attribute descriptor that derives its worth dynamically from one other attribute.
- Find out how to construct a lazily loaded attribute descriptor that defers costly computations till the attribute is first accessed.

Introduction and Motivation
In Python, descriptors are a mechanism that governs what happens once we entry an object’s attribute, set it, or take away it. They’re outlined by a robust protocol whereby a category implements particular strategies like __get__, __set__, and so forth, to customise entry to the attribute(s).
In the meantime, managed attributes are properties outlined on the class stage whose write, learn, and delete logic is dealt with by the aforesaid descriptors. Mastering descriptors is the important thing to understanding the mechanics behind many well-known Python libraries and configuration techniques. We don’t intend to go that deep on this article, however we are going to present you methods to use Python to construct three kinds of managed attributes by descriptors: computed, validated, and lazily loaded attributes.
And the way will we study this? By means of an instance that simulates monitoring information in a fictitious Japan tourism utility, exhibiting that descriptors can be utilized to neatly handle attribute states with out resorting to code-cluttering @property decorators, getters, and setters.
1. Constructing Validated Attributes by Descriptors
At the start, in Python programming phrases, a descriptor is a category that implements strategies like __get__, __set__, or __delete__ to outline conduct on the attribute stage. Not all of them should be carried out, as we are going to see. There may be additionally one other technique launched in Python 3.6, known as __set_name__, which lets the descriptor know the title of the attribute it’s being assigned to — one thing helpful for storing information inside a mother or father object’s __dict__ whereas avoiding title clashes.
In a nutshell, the next code defines a descriptor known as ValidatedRating that permits straightforward and sturdy validation of knowledge inputs on class attributes, verifying particularly that assigned scores meet particular kind and vary necessities:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class ValidatedRating: def __init__(self, min_val=1.0, max_val=5.0): self.min_val = min_val self.max_val = max_val
def __set_name__(self, proprietor, title): # Robotically save the attribute title (e.g., ‘ranking’) self.public_name = title self.private_name = f“_{title}”
def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.private_name)
def __set__(self, obj, worth): if not isinstance(worth, (int, float)): elevate TypeError(f“{self.public_name} have to be a quantity.”) if not (self.min_val <= worth <= self.max_val): elevate ValueError(f“{self.public_name} have to be between {self.min_val} and {self.max_val}.”) setattr(obj, self.private_name, float(worth)) |
We are going to see this descriptor in motion along with the opposite two as soon as we’ve outlined all of them.
2. Constructing Computed Attributes by Descriptors
Computed attribute descriptors are helpful in conditions like dynamically calculating an attribute’s worth primarily based on one other object’s state. As an example, if in our Japan tourism app (carried out shortly) we would like costs saved in Japanese Yen (JPY) however must show them in US {Dollars}, a descriptor can develop into notably helpful — particularly by implementing a __get__ technique that calculates the foreign money conversion on the fly:
|
class ComputedUSDPrice: def __init__(self, exchange_rate=0.0067): # 1 JPY = ~0.0067 USD self.exchange_rate = exchange_rate
def __set_name__(self, proprietor, title): self.title = title
def __get__(self, obj, objtype=None): if obj is None: return self # Fetch the bottom JPY worth from the mother or father object jpy_price = getattr(obj, ‘price_jpy’, 0) return spherical(jpy_price * self.exchange_rate, 2)
def __set__(self, obj, worth): elevate AttributeError(f“{self.title} is a computed read-only attribute.”) |
3. Constructing Lazily Loaded Attributes by Descriptors
The third technique is beneficial for attributes that require costly computations, e.g. studying a big JSON file or querying a big database for Shinkansen (Japanese bullet practice) timetables. In these instances, lazily loaded attributes merely defer loading till the info is explicitly requested. A descriptor primarily based on this coverage, additionally known as a lazy descriptor, calculates the worth on first entry, saves it to the __dict__ of your occasion, and steps apart. After that, future entry bypasses the descriptor solely.
The next class implements lazy loading, caching information that is likely to be pricey to generate or retrieve:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import time
class LazyItinerary: def __set_name__(self, proprietor, title): self.title = title
def __get__(self, obj, objtype=None): if obj is None: return self
print(f“[System] Simulating costly database question for {obj.vacation spot} itinerary…”) time.sleep(1) # Simulate community/database latency
# Generate the heavy payload itinerary = ( f“Day 1: Arrival in {obj.vacation spot}, check-in.n” f“Day 2: Guided temple tour and native road meals tasting.n” f“Day 3: Shinkansen departure.” )
# Cache it immediately on the item’s dictionary. # Subsequent time obj.itinerary is known as, Python finds it right here first! obj.__dict__[self.name] = itinerary return itinerary |
Placing It All Collectively
Let’s see our three descriptors in motion! The next code applies them to a category named JapanTour, whose attributes are every ruled by one among our three customized descriptors.
|
class JapanTour: # Connect our descriptors on the class stage ranking = ValidatedRating(min_val=1.0, max_val=5.0) price_usd = ComputedUSDPrice() itinerary = LazyItinerary()
def __init__(self, vacation spot, price_jpy, ranking): self.vacation spot = vacation spot self.price_jpy = price_jpy self.ranking = ranking # This triggers ValidatedRating.__set__ |
Now that we’ve a tangible class with real-world-like attributes, the summary descriptors we outlined earlier make much more sense. Within the constructor, any attribute backed by a descriptor that defines __set__ will invoke that technique instantly throughout initialization.
Let’s instantiate a JapanTour object and run a primary take a look at:
|
# 1. Initializing the tour (Triggers ranking validation) tour = JapanTour(vacation spot=“Kyoto”, price_jpy=45000, ranking=4.8)
# 2. Testing the Validator print(f“Vacation spot: {tour.vacation spot}”) print(f“Preliminary Ranking: {tour.ranking}”) attempt: tour.ranking = 6.0 besides ValueError as e: print(f“Validation caught error: {e}”) |
Not solely can we entry attributes, however we additionally attempt assigning an invalid worth — one which exceeds the vary enforced by our validated ranking descriptor — which leads to an exception being caught:
|
Vacation spot: Kyoto Preliminary Ranking: 4.8 Validation caught error: ranking should be between 1.0 and 5.0. |
Lastly, we take a look at the computed descriptor and the lazy descriptor:
|
# 3. Testing the Computed Property print(f“Value (JPY): ¥{tour.price_jpy}”) print(f“Value (USD): ${tour.price_usd}”)
# 4. Testing the Lazy Loader print(“n— Requesting Itinerary (First Time) —“) print(tour.itinerary)
print(“n— Requesting Itinerary (Second Time) —“) print(tour.itinerary) |
Output:
|
—– Requesting Itinerary (First Time) —– [System] Simulating costly database question for Kyoto itinerary... Day 1: Arrival in Kyoto, verify–in. Day 2: Guided temple tour and native road meals tasting. Day 3: Shinkansen departure.
—– Requesting Itinerary (Second Time) —– Day 1: Arrival in Kyoto, verify–in. Day 2: Guided temple tour and native road meals tasting. Day 3: Shinkansen departure. |
Discover that on the second request, there isn’t any system loading message — the lazy descriptor has already cached the end result and Python reads it immediately from the occasion’s __dict__.
Wrapping Up
We’ve proven methods to make customized class attributes considerably extra sturdy in real-world functions utilizing Python descriptors. The three sorts lined — validated, computed, and lazily loaded — handle information integrity constraints, dynamically derived values, and costly information operations, respectively.

