Python __set_name__ Practice Problems & Exercises
Practice: __set_name__
← Back to lessonEasy
Create a Field descriptor that uses __set_name__ to learn its own attribute name. Print the captured names at class creation time, then use them in get/set.
class Field:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = "_field_" + name
print(f"Descriptor public name: {name}")
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
setattr(obj, self.private_name, value)
class User:
username = Field()
email = Field()
def __init__(self, username, email):
self.username = username
self.email = email
u = User("alice", "[email protected]")
print(f"username = {u.username}")
print(f"email = {u.email}")Expected Output
Descriptor public name: username
Descriptor public name: email
username = alice
email = [email protected]Hints
Hint 1: __set_name__(self, owner, name) is called automatically by type when the class body is processed.
Hint 2: Store name as self.public_name and "_" + name as self.private_name for the instance storage key.
Build a TrackedField descriptor that logs both its name and its owner class during __set_name__.
class TrackedField:
def __set_name__(self, owner, name):
self.name = name
self.owner_name = owner.__name__
self.private = f"_{owner.__name__}_{name}"
print(f"Field '{name}' registered on class '{owner.__name__}'")
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
setattr(obj, self.private, value)
class Employee:
salary = TrackedField()
def __init__(self, salary):
self.salary = salary
class Department:
budget = TrackedField()
def __init__(self, budget):
self.budget = budget
e = Employee(85000)
d = Department(500000)
print(f"Employee.salary = {e.salary}")
print(f"Department.budget = {d.budget}")Expected Output
Field 'salary' registered on class 'Employee'
Field 'budget' registered on class 'Department'
Employee.salary = 85000
Department.budget = 500000Hints
Hint 1: __set_name__ receives owner — the class where the descriptor was assigned.
Hint 2: You can store owner.__name__ for logging or build per-owner storage keys.
Demonstrate that a single descriptor class can be safely reused across multiple unrelated classes without instance value bleeding.
class Speed:
def __set_name__(self, owner, name):
self.name = name
# Include owner name to avoid key collisions
self.private = f"_{owner.__name__}__speed"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, 0)
def __set__(self, obj, value):
if value < 0:
raise ValueError("Speed cannot be negative")
object.__setattr__(obj, self.private, value)
class Car:
speed = Speed()
def __init__(self, speed):
self.speed = speed
class Bike:
speed = Speed()
def __init__(self, speed):
self.speed = speed
car = Car(120)
bike = Bike(35)
print(f"car.speed = {car.speed}")
print(f"bike.speed = {bike.speed}")
# Mutate one and confirm the other is unaffected
car.speed = 200
assert bike.speed == 35, "Bleeding detected!"
print("No instance bleeding between Car and Bike")Expected Output
car.speed = 120
bike.speed = 35
No instance bleeding between Car and BikeHints
Hint 1: The same descriptor instance is shared across class definitions if you assign the same object. __set_name__ is called separately for each class.
Hint 2: Use object.__setattr__ with a class-namespaced private key so instances of different classes never share storage.
Medium
Create a DocField descriptor that registers itself with its owner so the class can print a formatted schema of all its fields.
class DocField:
def __init__(self, field_type, doc=""):
self.field_type = field_type
self.doc = doc
self.name = None
def __set_name__(self, owner, name):
self.name = name
self.private = "_df_" + name
# Register on owner
if not hasattr(owner, "_fields"):
owner._fields = []
owner._fields.append((name, self.field_type.__name__, self.doc))
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
if not isinstance(value, self.field_type):
raise TypeError(f"{self.name} must be {self.field_type.__name__}")
setattr(obj, self.private, value)
@classmethod
def describe(cls, klass):
print(f"{klass.__name__} fields:")
for name, type_name, doc in getattr(klass, "_fields", []):
print(f" {name} ({type_name}): {doc}")
class Config:
host = DocField(str, "hostname or IP of the server")
port = DocField(int, "port number (1-65535)")
debug = DocField(bool, "enable verbose debug output")
def __init__(self, host, port, debug=False):
self.host = host
self.port = port
self.debug = debug
DocField.describe(Config)
c = Config("localhost", 8080, False)
print(f"host={c.host}, port={c.port}, debug={c.debug}")Expected Output
Config fields:
host (str): hostname or IP of the server
port (int): port number (1-65535)
debug (bool): enable verbose debug output
host=localhost, port=8080, debug=FalseHints
Hint 1: In __set_name__, append field metadata to owner._fields — a list of (name, type, doc) tuples.
Hint 2: A classmethod or __init_subclass__ can initialize _fields on the owner before __set_name__ is called.
Build a DefaultFactory descriptor that creates a fresh default value by calling a factory function on first access. Show it solves the classic mutable-default-argument bug.
class DefaultFactory:
def __init__(self, factory):
self.factory = factory
self.name = None
def __set_name__(self, owner, name):
self.name = name
self.private = "_lazy_" + name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if not hasattr(obj, self.private):
# Create a fresh default per instance
object.__setattr__(obj, self.private, self.factory())
return getattr(obj, self.private)
def __set__(self, obj, value):
object.__setattr__(obj, self.private, value)
class User:
tags = DefaultFactory(list)
def __init__(self, name):
self.name = name
user1 = User("Alice")
user2 = User("Bob")
user1.tags.append("admin")
user2.tags.append("guest")
print(f"user1 tags: {user1.tags}")
print(f"user2 tags: {user2.tags}")
print(f"Lists are different objects: {user1.tags is not user2.tags}")Expected Output
user1 tags: ['admin']
user2 tags: ['guest']
Lists are different objects: TrueHints
Hint 1: Store a factory callable (e.g., list) in the descriptor, not the default value itself.
Hint 2: In __get__, if the private attr is missing, call the factory, store the result, and return it. This avoids the mutable-default pitfall.
Create a PositiveField descriptor. Use __set_name__ to register all validated fields on the owner class and auto-generate __repr__.
class PositiveField:
def __init__(self, field_type=float, min_val=0):
self.field_type = field_type
self.min_val = min_val
self.name = None
def __set_name__(self, owner, name):
self.name = name
self.private = "_pf_" + name
if not hasattr(owner, "_pf_fields"):
owner._pf_fields = []
owner._pf_fields.append(name)
# Auto-generate __repr__ if not already defined by user
fields_ref = owner._pf_fields
def auto_repr(self_inner):
parts = [f"{f}={getattr(self_inner, f)!r}" for f in fields_ref]
return f"{owner.__name__}(" + ", ".join(parts) + ")"
if "__repr__" not in owner.__dict__:
owner.__repr__ = auto_repr
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
if not isinstance(value, self.field_type):
value = self.field_type(value)
if value < self.min_val:
raise ValueError(
f"ValidationError: {self.name} must be >= {self.min_val}, got {value}"
)
setattr(obj, self.private, value)
class Product:
name: str
price = PositiveField(float, min_val=0)
quantity = PositiveField(int, min_val=0)
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price}, quantity={self.quantity})"
p = Product("Widget", 9.99, 100)
print(f"Valid: {p}")
try:
p.price = -5
except ValueError as e:
print(e)
try:
p.quantity = -1
except ValueError as e:
print(e)Expected Output
Valid: Product(name='Widget', price=9.99, quantity=100)
ValidationError: price must be >= 0, got -5
ValidationError: quantity must be >= 0, got -1Hints
Hint 1: Accept min_val and max_val in the descriptor __init__. Validate in __set__.
Hint 2: Register each field on the owner via a _schema list so you can build __repr__ automatically.
Build a CountedField descriptor that counts how many times each attribute is read on a per-instance basis. Expose the counts via a access_counts() method.
class CountedField:
def __set_name__(self, owner, name):
self.name = name
self.private_val = "_cf_val_" + name
self.private_cnt = "_cf_cnt_" + name
# Register field name for the counts collector
if not hasattr(owner, "_cf_field_names"):
owner._cf_field_names = []
owner._cf_field_names.append(name)
def __get__(self, obj, objtype=None):
if obj is None:
return self
count = getattr(obj, self.private_cnt, 0) + 1
object.__setattr__(obj, self.private_cnt, count)
return getattr(obj, self.private_val, None)
def __set__(self, obj, value):
object.__setattr__(obj, self.private_val, value)
def access_counts(obj):
result = {}
for field in getattr(type(obj), "_cf_field_names", []):
cnt_key = "_cf_cnt_" + field
result[field] = getattr(obj, cnt_key, 0)
return result
class Profile:
name = CountedField()
email = CountedField()
def __init__(self, name, email):
self.name = name
self.email = email
p = Profile("Alice", "[email protected]")
# Access name three times, email once
_ = p.name
_ = p.name
_ = p.name
_ = p.email
counts = access_counts(p)
print(f"name accessed {counts['name']} times")
print(f"email accessed {counts['email']} time")
print(counts)Expected Output
name accessed 3 times
email accessed 1 time
{'name': 3, 'email': 1}Hints
Hint 1: Store a counter dict in each instance using a private key set during __set_name__.
Hint 2: In __get__, look up and increment the counter before returning the value.
Hard
Build a WriteOnce descriptor that allows the first assignment freely but raises a custom FrozenError on any subsequent write. Use __set_name__ for the attribute name in error messages.
class FrozenError(Exception):
pass
class WriteOnce:
def __set_name__(self, owner, name):
self.name = name
self.private_val = "_wo_val_" + name
self.private_set = "_wo_set_" + name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_val, None)
def __set__(self, obj, value):
if getattr(obj, self.private_set, False):
raise FrozenError(f"'{self.name}' can only be assigned once")
object.__setattr__(obj, self.private_val, value)
object.__setattr__(obj, self.private_set, True)
class APICredential:
token = WriteOnce()
def __init__(self, token):
self.token = token
cred = APICredential("abc123")
print(f"token = {cred.token}")
try:
cred.token = "xyz789"
except FrozenError as e:
print(f"FrozenError: {e}")Expected Output
token = abc123
FrozenError: 'token' can only be assigned onceHints
Hint 1: Track whether the value has been set using a boolean flag stored in the instance dict.
Hint 2: In __set__, check the flag. If already set, raise a custom FrozenError instead of AttributeError to distinguish the intent.
Design a Coerced descriptor that auto-casts values to the target type on assignment. It also registers a JSON-serializable schema on the owner class and provides a serialize() helper.
class Coerced:
def __init__(self, target_type):
self.target_type = target_type
self.name = None
def __set_name__(self, owner, name):
self.name = name
self.private = "_co_" + name
if not hasattr(owner, "_schema"):
owner._schema = {}
owner._schema[name] = target_type.__name__
# Attach serialize() once
if "serialize" not in owner.__dict__:
schema_ref = owner._schema
def serialize(self_inner):
return {
field: getattr(self_inner, field)
for field in schema_ref
}
owner.serialize = serialize
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
try:
coerced = self.target_type(value)
except (TypeError, ValueError) as exc:
raise TypeError(
f"Cannot coerce {value!r} to {self.target_type.__name__} for '{self.name}': {exc}"
)
object.__setattr__(obj, self.private, coerced)
class Record:
id = Coerced(int)
score = Coerced(float)
active = Coerced(bool)
def __init__(self, id, score, active):
self.id = id
self.score = score
self.active = active
def __repr__(self):
return f"Record(id={self.id}, score={self.score}, active={self.active})"
r = Record("42", "9.5", 1) # strings coerced
print(r)
print(f"Schema: {Record._schema}")
print(f"Serialized: {r.serialize()}")Expected Output
Record(id=42, score=9.5, active=True)
Schema: {'id': 'int', 'score': 'float', 'active': 'bool'}
Serialized: {'id': 42, 'score': 9.5, 'active': True}Hints
Hint 1: Accept a target_type in __init__ and attempt value = target_type(value) in __set__. Wrap in try/except to provide a clean error.
Hint 2: Register (name, type_name) on owner._schema in __set_name__. Add serialize() as a method on the class that reads _schema fields.
Build a ModelField descriptor registry that correctly handles inheritance — all_fields() on a subclass returns inherited fields from parent classes too, in definition order.
class ModelField:
def __set_name__(self, owner, name):
self.name = name
self.private = "_mf_" + name
if "_own_fields" not in owner.__dict__:
owner._own_fields = []
owner._own_fields.append(name)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private, None)
def __set__(self, obj, value):
object.__setattr__(obj, self.private, value)
@staticmethod
def all_fields(cls):
seen = set()
result = []
# Walk MRO from base to derived to preserve definition order
for klass in reversed(cls.__mro__):
for f in getattr(klass, "_own_fields", []):
if f not in seen:
seen.add(f)
result.append(f)
return result
class Animal:
name = ModelField()
age = ModelField()
def __init__(self, name, age):
self.name = name
self.age = age
class Dog(Animal):
breed = ModelField()
def __init__(self, name, age, breed):
super().__init__(name, age)
self.breed = breed
print(f"Animal fields: {ModelField.all_fields(Animal)}")
print(f"Dog fields (including inherited): {ModelField.all_fields(Dog)}")
rex = Dog("Rex", 3, "Labrador")
print(f"rex.name = {rex.name}, rex.breed = {rex.breed}")Expected Output
Animal fields: ['name', 'age']
Dog fields (including inherited): ['name', 'age', 'breed']
rex.name = Rex, rex.breed = LabradorHints
Hint 1: In __set_name__, append to owner._own_fields (only fields defined directly on that class).
Hint 2: A classmethod all_fields(cls) walks cls.__mro__ in reverse order collecting _own_fields from each ancestor to build an ordered, deduplicated list.
Build a BelongsTo descriptor that, when you assign employee.team = team_obj, automatically registers the employee in team.members and removes them from the old team's list.
class BelongsTo:
"""employee.team = team automatically updates team.members."""
def __init__(self, back_attr):
self.back_attr = back_attr # e.g., "members"
self.name = None
def __set_name__(self, owner, name):
self.name = name
self.private = "_bt_" + name
def __get__(self, obj, objtype=None):
if obj is None:
return self
ref = getattr(obj, self.private, None)
return ref
def __set__(self, obj, new_target):
old_target = getattr(obj, self.private, None)
# Remove from old target's back list
if old_target is not None:
back_list = getattr(old_target, self.back_attr, [])
if obj in back_list:
back_list.remove(obj)
object.__setattr__(obj, self.private, new_target)
# Add to new target's back list
if new_target is not None:
back_list = getattr(new_target, self.back_attr, None)
if back_list is None:
back_list = []
object.__setattr__(new_target, self.back_attr, back_list)
if obj not in back_list:
back_list.append(obj)
class Team:
def __init__(self, name):
self.name = name
self.members = []
def __repr__(self):
return self.name
class Employee:
team = BelongsTo(back_attr="members")
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
eng = Team("Engineering")
product = Team("Product")
alice = Employee("Alice")
bob = Employee("Bob")
alice.team = eng
print(f"alice.team = {alice.team}")
print(f"team.members: {[e.name for e in eng.members]}")
bob.team = eng
print(f"bob.team = {bob.team}")
print(f"team.members: {[e.name for e in eng.members]}")
alice.team = product
print(
f"After reassign: alice.team = {alice.team}, "
f"Engineering members: {[e.name for e in eng.members]}"
)Expected Output
alice.team = Engineering
team.members: ['Alice']
bob.team = Engineering
team.members: ['Alice', 'Bob']
After reassign: alice.team = Product, Engineering members: ['Bob']Hints
Hint 1: The descriptor stores a reference to both the forward object and enables back-population on the related class.
Hint 2: In __set__, find the old value (if any) and remove self.obj from the related class reverse list. Then set the new value and append obj to the new target reverse list.
