Metaclasses & Descriptors
There is no TypeScript equivalent — and that is the point
Section titled “There is no TypeScript equivalent — and that is the point”Descriptors and metaclasses are two of Python’s most powerful runtime features. TypeScript has decorators (stage-3, experimental), but they are much more limited. Python’s descriptor protocol and metaclass system operate at a lower level: they control how attribute access and class creation work at runtime. Understanding them explains how @property, @dataclass, and ORMs like SQLAlchemy actually work under the hood.
Descriptors: controlled attribute access
Section titled “Descriptors: controlled attribute access”A descriptor is any object that defines __get__, __set__, or __delete__. When a descriptor instance is assigned as a class attribute, Python calls these methods instead of doing a plain dictionary lookup every time you access the attribute on an instance.
// TypeScript — closest analogy: getter/setterclass Temperature { private _celsius: number;
constructor(celsius: number) { this._celsius = celsius; }
get fahrenheit(): number { return this._celsius * 9/5 + 32; }
set fahrenheit(value: number) { this._celsius = (value - 32) * 5/9; }}
const t = new Temperature(100);console.log(t.fahrenheit); // 212t.fahrenheit = 32;console.log(t.fahrenheit); // 32# Python — descriptor protocol (reusable across classes)class Validated: """Descriptor: ensures field is numeric.""" def __set_name__(self, owner, name): self._name = name # called at class creation
def __get__(self, obj, objtype=None): if obj is None: return self # class-level access return getattr(obj, f"_{self._name}", None)
def __set__(self, obj, value): if not isinstance(value, (int, float)): raise TypeError( f"{self._name} must be numeric, " f"got {type(value).__name__}" ) setattr(obj, f"_{self._name}", value)
class Product: price = Validated() # descriptor instance as class attr quantity = Validated()
def __init__(self, price: float, quantity: int) -> None: self.price = price self.quantity = quantity
def total(self) -> float: return self.price * self.quantity
item = Product(9.99, 3)print(item.total()) # 29.97item.price = "free" # TypeError: price must be numericThe key insight: the Validated descriptor can be reused on any number of classes and any number of attributes. TypeScript getters/setters are per-class and per-property — you cannot share one definition.
set_name: descriptor self-registration
Section titled “set_name: descriptor self-registration”__set_name__ is called by Python at class creation time (when the class body is executed), giving the descriptor the name of the attribute it was assigned to. This is why you do not need to write price = Validated("price") — Python injects the name automatically.
Try it — descriptor demo
Section titled “Try it — descriptor demo”class Validated: def __set_name__(self, owner, name): self._name = name
def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, f"_{self._name}", None)
def __set__(self, obj, value): if not isinstance(value, (int, float)): raise TypeError( f"{self._name} must be numeric, got {type(value).__name__}" ) setattr(obj, f"_{self._name}", value)
class Product: price = Validated() quantity = Validated()
def __init__(self, price: float, quantity: int) -> None: self.price = price self.quantity = quantity
def total(self) -> float: return self.price * self.quantity
item = Product(9.99, 3)print(f"Total: {item.total()}")
item.price = 19.99print(f"Updated total: {item.total()}")
try: item.price = "free"except TypeError as e: print(f"Caught: {e}")Loading Python runtime (first run only)…