TypeError: Descriptors Cannot Be Created Directly
The error message "TypeError: descriptors cannot be created directly" typically appears when working with Python’s descriptor protocol, particularly when attempting to instantiate a built-in descriptor type such as property, staticmethod, or classmethod without properly invoking its constructor. Understanding this error requires familiarity with how descriptors work in Python, why they exist, and how to use them correctly. This leads to this error often arises in object-oriented programming scenarios where developers define classes that rely on descriptors to manage attribute access dynamically. In this article, we’ll explore what descriptors are, why this specific TypeError occurs, and how to resolve it effectively Worth knowing..
What Are Descriptors in Python?
Descriptors are a fundamental concept in Python’s object model that allow objects to customize attribute access. In practice, they are implemented using special methods like __get__, __set__, and __delete__. When you access an attribute on an instance, Python checks if that attribute is a descriptor and calls the appropriate method accordingly That's the part that actually makes a difference. Which is the point..
There are two main types of descriptors:
- Data descriptors: These define both
__get__and__set__(or__delete__), giving them priority over instance dictionaries. - Non-data descriptors: These only define
__get__, meaning they have lower precedence than instance variables.
Common examples of built-in descriptors include:
propertystaticmethodclassmethod
These types are part of Python’s standard library and are used extensively in class definitions Most people skip this — try not to..
Why Does the Error Occur?
The error "TypeError: descriptors cannot be created directly" usually happens when someone tries to create a new instance of a built-in descriptor type outside of their intended usage context. As an example, consider the following incorrect code snippet:
p = property()
While this might seem valid at first glance, it actually raises the TypeError because property() expects certain arguments during initialization—specifically getter, setter, and deleter functions. Attempting to call property() without these arguments results in the error Easy to understand, harder to ignore. Still holds up..
Similarly, trying to do something like this:
s = staticmethod()
will also raise the same error since staticmethod is not meant to be instantiated directly.
In general, built-in descriptor types require explicit construction through their respective constructors with proper parameters. Directly calling them without supplying required inputs leads to the TypeError Surprisingly effective..
Common Scenarios That Trigger This Error
Let’s look at some real-world situations where this error commonly surfaces:
1. Incorrect Property Instantiation
Attempting to create a property without defining any behavior:
class MyClass:
my_property = property()
obj = MyClass()
print(obj.my_property)
This will result in:
TypeError: descriptors cannot be created directly.
To fix this, you must provide at least a getter function:
class MyClass:
def get_value(self):
return self._value
my_property = property(get_value)
Now, accessing my_property will work correctly.
2. Misusing Staticmethod or Classmethod
Creating instances of staticmethod or classmethod manually instead of applying them as decorators:
class MyClass:
my_static_method = staticmethod()
MyClass().my_static_method()
Again, this triggers the TypeError. Instead, apply it as a decorator:
class MyClass:
@staticmethod
def my_static_method():
print("Hello from static method")
MyClass.my_static_method()
3. Custom Descriptor Classes Without Proper Implementation
If you're creating your own descriptor class, check that you implement the necessary dunder methods correctly. Failing to do so may lead to confusion or unexpected behavior, though not necessarily the exact same TypeError unless mimicking built-ins improperly Small thing, real impact..
How to Fix the TypeError
Fixing this error involves understanding the correct way to use and instantiate descriptors. Here are several approaches based on different contexts:
Use Decorators Properly
Instead of manually instantiating descriptors, always use them via decorators:
class MyClass:
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = val
This approach ensures that the descriptor machinery works as expected.
Pass Required Functions During Initialization
When constructing a property, pass the relevant functions:
def getter(self):
return self._x
def setter(self, val):
self._x = val
my_prop = property(getter, setter)
Then assign it inside a class:
class MyClass:
x = my_prop
Avoid Manual Instantiation of Built-In Descriptors
Never try to create instances of staticmethod, classmethod, or property outside of their standard usage patterns. Always use them within class bodies or as decorators Worth keeping that in mind. Still holds up..
Best Practices for Working with Descriptors
To avoid running into this error and maintain clean, readable code, follow these best practices:
- Use decorators: make use of
@property,@staticmethod, and@classmethodinstead of manually instantiating them. - Implement full descriptor protocol: If creating custom descriptors, implement all relevant methods (
__get__,__set__,__delete__) appropriately. - Understand scope and lifecycle: Know when descriptors are evaluated and applied—typically at class definition time.
- Avoid premature abstraction: Don’t overcomplicate simple attribute management by introducing unnecessary layers of indirection unless needed.
Frequently Asked Questions (FAQ)
Q: Can I create a custom descriptor that mimics property?
A: Yes, but you must implement the full descriptor protocol correctly. Mimicking built-in behavior requires careful attention to method signatures and return values.
Q: Is there ever a reason to instantiate property() directly?
A: Only when passing pre-defined getter/setter/deleter functions explicitly. Otherwise, prefer using the decorator syntax.
Q: Does this error occur in other languages?
A: No, this is specific to Python due to its unique descriptor protocol implementation.
Q: How can I debug descriptor-related issues?
A: Check whether you’re using built-in descriptors correctly, verify that all required functions are passed, and inspect the object model using tools like inspect Surprisingly effective..
Conclusion
Understanding the "TypeError: descriptors cannot be created directly" error is crucial for effective Python development, especially when dealing with advanced object-oriented features. By recognizing the role of descriptors and adhering to best practices around their usage, developers can write more solid and maintainable code. Remember to put to work Python’s powerful decorator system, avoid manual instantiation of built-in descriptors, and thoroughly test your implementations to prevent such errors from occurring. With practice, mastering descriptors becomes second nature and enhances your overall proficiency in Python programming.
Practical Example: Building a CachedProperty Descriptor
One of the most common uses of custom descriptors is to implement a cached property—computing a value once and storing it for subsequent accesses. Below is a compact, production‑ready implementation that demonstrates the descriptor protocol in action:
class CachedProperty:
"""Descriptor that computes a value on first access and caches it on the instance."""
def __init__(self, func):
self.func = func
self.attr_name = f"_{func.
def __get__(self, instance, owner):
if instance is None:
return self
# If the cached value exists, return it directly.
Here's the thing — if hasattr(instance, self. attr_name):
return getattr(instance, self.
# Otherwise compute, store, and return the value.
Here's the thing — value = self. func(instance)
setattr(instance, self.
def __set_name__(self, owner, name):
# Allow the descriptor to know the attribute name under which it is stored.
self.attr_name = f"_{name}_cache"
def __delete__(self, instance):
raise AttributeError(f"Cannot delete cached property {self.func.__name__!
**How it works**
1. **Initialization (`__init__`)** – The original getter function is stored, and a private attribute name is derived to hold the cached result.
2. **Retrieval (`__get__`)** – On the first call, the underlying function is invoked with the instance, its result is saved, and subsequent accesses bypass the computation.
3. **Name binding (`__set_name__`)** – Introduced in Python 3.6, this method lets the descriptor learn the name it was assigned to in the class body, making the implementation more flexible.
4. **Deletion (`__delete__`)** – Raising an `AttributeError` prevents accidental deletion of the cached value, preserving the semantics of a read‑only property.
**Using the descriptor**
```python
class Circle:
def __init__(self, radius):
self.radius = radius
@CachedProperty
def area(self):
print("Computing area…")
return 3.14159 * self.radius ** 2
c = Circle(5)
print(c.Worth adding: area) # Prints "Computing area…" then 78. But 53975
print(c. area) # No print, directly returns 78.
The `CachedProperty` example illustrates how a well‑crafted descriptor can encapsulate complex behavior while remaining transparent to the user. It also underscores the importance of implementing the full protocol—`__get__`, `__set_name__`, and `__delete__`—to avoid subtle bugs.
---
### Further Reading & Resources
- **Official Documentation** – The chapter provides a rigorous definition of descriptors and their methods.
- **“Fluent Python” (Luciano Ramalho)** – Chapters on descriptors and the `@property` decorator offer deep insights and idiomatic patterns.
- **PEP 487 – “Simplifying Metaclass Usage”** – Introduces `__set_name__`, a feature leveraged by the `CachedProperty` example.
- **Real‑World Libraries** – Explore how frameworks such as SQLAlchemy, Django, and attrs employ custom descriptors for validation, lazy loading, and ORM mapping.
---
### Closing Thoughts
Descriptors are a cornerstone of Python’s object‑oriented design, enabling developers to craft elegant, reusable, and performant attribute behaviors. While the built‑in descriptors (`property`, `staticmethod`, `classmethod`) shield most of us from low‑level details, understanding the underlying protocol empowers you to build sophisticated abstractions—like cached properties, computed fields, or validation wrappers—without resorting
Beyond the elementary examples, descriptors become truly powerful when they are **parameterized**, **class‑aware**, or **instrumented** for specific concerns.
**Parameterized descriptors** let a single class implement many behaviours by passing arguments to the descriptor’s constructor. A typical use‑case is a caching descriptor that accepts a *time‑to‑live* (TTL) value:
```python
class TTLCache:
def __init__(self, ttl):
self.ttl = ttl # seconds the value remains valid
self._store = {} # instance‑level cache
def __get__(self, obj, objtype=None):
if obj is None:
return self
now = time.Even so, get(key)
if entry and (now - entry['time']) < self. time()
key = (obj, self.Still, ttl:
return entry['value']
# cache miss – compute, store, and return
value = self. Because of that, _store. Because of that, _cache_key())
entry = self. _compute(obj)
self.
You'll probably want to bookmark this section.
def _cache_key(self):
# simple example; real code may incorporate more context
return f'{self.Because of that, __class__. __name__}_{obj.
Because the descriptor holds its own state (`self.ttl` and `self._store`), each instance can have an independent cache policy without any extra code in the class body.
**Data vs. non‑data descriptors** is another axis of variation. A *data descriptor* implements `__set__` (or `__delete__`) and therefore overrides any attribute value assigned to the name, regardless of the class’s inheritance chain. A *non‑data descriptor* (only `__get__`) can be shadowed by a plain attribute on the instance or on a subclass. Understanding this distinction prevents surprising bugs when subclasses introduce their own attributes with the same name.
**Multiple inheritance** adds another layer of complexity. The Method Resolution Order (MRO) determines which descriptor wins when several descriptors target the same attribute name. If a base class defines a data descriptor for `value` and a subclass defines a non‑data descriptor for the same name, instances will see the subclass’s descriptor, while the class will still use the base‑class descriptor for un‑bound lookups. Explicitly documenting the intended precedence (e.g., “the first descriptor in the MRO that implements `__set__` is authoritative”) helps maintain predictability.
**Class‑level caching** is a pattern that many frameworks employ. By storing the cached result on the *class* rather than on each instance, you avoid redundant computation when many objects share the same inputs. A descriptor that reads a class attribute (e.g., a configuration object) and caches the derived value can be written as:
```python
class ClassCache:
def __init__(self, func):
self.func = func
self._value = None
def __get__(self, obj, objtype=None):
if objtype is None: # accessed via the class
return self
if self.Plus, _value = self. _value is None:
self.func(objtype)
return self.
Because the cache lives on the descriptor itself, every subclass inherits the same cached value unless it overrides the underlying function.
**Thread‑safety** is often overlooked. When multiple threads may invoke the descriptor concurrently, a simple assignment in `__set__` can race with a concurrent read in `__get__`. Wrapping the critical sections with a `threading.Lock` (or using `collections.defaultdict` with a lock‑protected dict) guarantees that the cached value is either fully written or fully read, eliminating half‑initialized states.
**Validation and invariants** are another domain where descriptors excel. A descriptor can intercept `__set__`, verify the incoming value, and either raise `ValueError` or coerce it to a canonical representation. To give you an idea, a `PositiveFloat` descriptor could enforce that the stored number is strictly greater than zero:
```python
class PositiveFloat:
def __get__(self, obj, objtype=None):
return self._value
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError('must be a number')
if value <= 0:
raise ValueError('value must be positive')
self._value = float(value)
Such descriptors can be reused across many classes, providing a declarative way to enforce domain rules without sprinkling validation code throughout the code base.
Lazy recomputation under conditional triggers is useful when the derived value depends on external state that may change. A descriptor can store a dirty flag and recompute only when that flag is set. Take this case: a Temperature descriptor could cache the Celsius conversion but recompute when the underlying Fahrenheit value changes or when a user‑provided conversion factor is updated.
Descriptor factories that return a class of descriptors are another advanced technique. By using a closure or a class method, you can inject per‑instance configuration while keeping the descriptor’s implementation tidy. The factory pattern also encourages separation of concerns: the descriptor’s logic stays pure, while the factory handles binding of runtime parameters No workaround needed..
Attribute aliasing and proxying are more exotic but illustrative. A descriptor can expose a different attribute name to the outside world while internally delegating to another attribute. This is handy for creating backward‑compatible APIs or for wrapping third‑party objects without subclassing them And that's really what it comes down to..
Versioning and deprecation can be encoded in a descriptor. By storing a version number alongside the attribute value, the descriptor can issue warnings when older code accesses a feature that will be removed, giving developers a graceful migration path That's the part that actually makes a difference. Worth knowing..
Conversion descriptors automatically transform incoming values before storing them. A UrlNormalizer descriptor could accept a raw string, validate it as a URL, and store a canonical representation, thereby centralising URL handling logic.
Attribute delegation lets a descriptor forward attribute access to another object, enabling thin wrapper classes that expose a clean public interface while delegating most work to an internal component.
Composition of descriptors—where one descriptor wraps another—creates layered behaviours such as logging, caching, and validation in a single attribute Which is the point..
Integration with data‑class libraries also benefits from custom descriptors. In dataclasses, the generated __init__ method interacts with descriptors that appear as field values. By defining a descriptor that implements __set__, you can intercept the assignment that the dataclass’s constructor performs, allowing for post‑assignment side effects or normalization.
The attrs library takes this a step further: descriptors can be annotated with validators, converters, and default factories, and the library automatically wires them into the generated class. Understanding the descriptor protocol lets you extend attrs behaviour without abandoning its declarative style.
pydantic (and its successor pydantic‑v2) relies heavily on descriptor‑like machinery for field validation, defaults, and computed properties. Custom descriptors can be used to inject domain‑specific validation or to expose computed fields that are not present in the underlying model schema Worth keeping that in mind. Turns out it matters..
Metaclass interaction rounds out the picture. A metaclass can inject descriptors into a class automatically, for example by scanning the class namespace for specially named attributes and converting them into descriptor instances. This technique powers many ORM frameworks that map database columns to descriptor objects The details matter here..
In all these scenarios, the common thread is that a descriptor encapsulates state and behavior that would otherwise be scattered across the class definition. Because of that, by adhering strictly to the descriptor protocol—providing __get__, optionally __set__ and __delete__, and, from Python 3. 6 onward, __set_name__—you obtain a clean, reusable building block that works without friction with Python’s attribute lookup mechanics.
Conclusion
Descriptors are more than a curiosity; they are a fundamental tool that underpins much of Python’s modern class system. On top of that, mastering the full protocol—including __set_name__, data‑descriptor semantics, and the nuances of inheritance and concurrency—empowers you to write code that is both expressive and efficient. Whether you are crafting cached properties, validation layers, class‑level caches, or sophisticated factories, the descriptor pattern provides a clean abstraction that keeps your classes focused on domain logic while delegating cross‑cutting concerns to reusable components. Embracing these patterns lets you build dependable, maintainable APIs and libraries that put to work Python’s dynamic nature without sacrificing clarity or performance Worth knowing..