python
37 lines · 8 steps
Building a typed descriptor in Python
A data descriptor that enforces types and runs custom validators on every attribute assignment.
Explained by
highlit
1class Typed:
2 """A data descriptor that enforces a type and optional validation."""
3
4 def __init__(self, expected_type, validator=None):
5 self.expected_type = expected_type
6 self.validator = validator
7
8 def __set_name__(self, owner, name):
9 self.name = name
10 self.private_name = f"_{name}"
11
12 def __get__(self, instance, owner=None):
13 if instance is None:
14 return self
15 return getattr(instance, self.private_name)
16
17 def __set__(self, instance, value):
18 if not isinstance(value, self.expected_type):
19 raise TypeError(
20 f"{self.name!r} must be {self.expected_type.__name__}, "
21 f"got {type(value).__name__}"
22 )
23 if self.validator is not None and not self.validator(value):
24 raise ValueError(f"{value!r} failed validation for {self.name!r}")
25 setattr(instance, self.private_name, value)
26
27 def __delete__(self, instance):
28 raise AttributeError(f"{self.name!r} cannot be deleted")
29
30
31class Account:
32 owner = Typed(str, validator=lambda s: len(s) > 0)
33 balance = Typed((int, float), validator=lambda n: n >= 0)
34
35 def __init__(self, owner, balance):
36 self.owner = owner
37 self.balance = balance
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Data descriptors intercept attribute access at the class level, letting you centralize validation logic instead of scattering checks across setters.
- 2__set_name__ hands each descriptor its own attribute name automatically, so one descriptor class can be reused for many fields.
- 3Storing values under a per-name private attribute keeps the descriptor's bookkeeping separate from the public attribute it guards.
Related explainers
typescript
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 steps
python
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session
Building a signup endpoint in FastAPI
dependency-injection
request-validation
background-tasks
Intermediate
8 steps
python
from django import forms from django.utils import timezone from .models import Reservation
Multi-field validation in a Django ModelForm
form validation
cross-field validation
modelform
Intermediate
7 steps
python
from django import template from django.urls import reverse, NoReverseMatch from django.utils.html import format_html
Active nav-link template tags in Django
template tags
url routing
active state
Intermediate
7 steps
python
from functools import wraps import asyncio from fastapi import APIRouter, FastAPI, Request
Per-route request timeouts in FastAPI
decorators
async
timeouts
Intermediate
6 steps
python
import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent
How a Django settings module is wired
configuration
environment-variables
middleware
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-typed-descriptor-in-python-explained-python-61cb/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.