Skip to main content

Python __set_name__ Practice Problems & Exercises

Practice: __set_name__

11 problems3 Easy4 Medium4 Hard65–80 min
← Back to lesson

Easy

#1Capture Attribute Name via __set_name__Easy
__set_name__descriptornaming

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.

Python
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.


#2Use Owner Class in __set_name__Easy
__set_name__ownerclass-introspection

Build a TrackedField descriptor that logs both its name and its owner class during __set_name__.

Python
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 = 500000
Hints

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.


#3Prevent Collision: Descriptor Reuse on Multiple ClassesEasy
__set_name__reusecollisionisolation

Demonstrate that a single descriptor class can be safely reused across multiple unrelated classes without instance value bleeding.

Python
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 Bike
Hints

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

#4Build a Self-Documenting DescriptorMedium
__set_name__documentationowner__doc__

Create a DocField descriptor that registers itself with its owner so the class can print a formatted schema of all its fields.

Python
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=False
Hints

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.


#5Lazy Default via __set_name__ and FactoryMedium
__set_name__lazydefault-factorydescriptor

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.

Python
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: True
Hints

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.


#6Schema Validator with __set_name__ + Owner RegistrationMedium
__set_name__schemavalidationowner-registration

Create a PositiveField descriptor. Use __set_name__ to register all validated fields on the owner class and auto-generate __repr__.

Python
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 -1
Hints

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.


#7Descriptor That Tracks Access Count Per InstanceMedium
__set_name__access-trackingper-instancecounter

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.

Python
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

#8Descriptor That Freezes After First AssignmentHard
__set_name__immutableonce-writabledescriptor

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.

Python
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 once
Hints

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.


#9Type-Coercing Descriptor with Schema ExportHard
__set_name__type-coercionschemaJSON-compatible

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.

Python
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.


#10Descriptor Registry with Owner-Class InheritanceHard
__set_name__inheritanceregistryMRO

Build a ModelField descriptor registry that correctly handles inheritance — all_fields() on a subclass returns inherited fields from parent classes too, in definition order.

Python
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 = Labrador
Hints

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.


#11Bidirectional Descriptor Linking Two ClassesHard
__set_name__bidirectionalrelationshipback-reference

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.

Python
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.

© 2026 EngineersOfAI. All rights reserved.