2025, Nov 19 15:00

Dynamic properties in Python: why loop-created getters keep reading the last attribute and how to fix it cleanly

Learn why Python dynamic properties built in loops bind to the last attribute due to late-binding closures, and fix them with a property factory using setattr.

Dynamic properties in Python: why your loop-created getters keep reading the last attribute

Dynamic attribute creation is convenient until the properties you add in a loop all start referencing the same name. A common pattern is to build a property per attribute using setattr, only to discover that every getter and setter appears to target the last item from the list. The temptation to fix it with eval is strong, but unnecessary. Here is what actually happens and how to solve it cleanly.

The failing pattern

The intent is straightforward: generate a property per attribute name and attach it to a class. The code below illustrates the idea.

fields = ["a", "b", "c"]
class Widget:
    pass
for name in fields:
    def read(self):
        return getattr(self, name)
    def write(self, value):
        setattr(self, name, value)
    # All properties end up bound to the last value in `name`
    setattr(Widget, name, property(read, write))

In contrast, wrapping setattr inside an eval that builds lambdas with inlined names appears to work as expected, but that is not the reasoned fix to the underlying problem.

What really goes wrong

Each loop iteration creates new functions, but they all capture the same outer-scope variable name. This is a late binding closure: the read and write callables reference the variable, not its value at definition time. When the loop finishes, the variable holds the final item from the list, so every generated property points to that last attribute.

There is a second trap here. If a property’s getter and setter read and write the same public attribute name, they will call themselves repeatedly and never reach a concrete value. The fix is to store data in a separate backing attribute, for example with a leading underscore.

The fix: bind the current name via a factory

The clean solution is to create a new scope per property so each getter and setter are closed over the right attribute name. A small helper that builds and returns a property achieves this. It also uses a distinct backing attribute to avoid infinite recursion.

def build_prop(field):
    hidden_name = f"_{field}"
    def read(self):
        return getattr(self, hidden_name)
    def write(self, value):
        setattr(self, hidden_name, value)
    return property(read, write)
fields = ["a", "b", "c"]
class Demo:
    pass
for field in fields:
    setattr(Demo, field, build_prop(field))
x1 = Demo()
x2 = Demo()
x1.a = 42
x1.b = 17
x2.a = 96
x2.b = 123
print(x1.a, x1.b, x1._a, x1._b)
print(x2.a, x2.b, x2._a, x2._b)

This produces the expected results, with each property correctly mapped to its own backing attribute.

Why this matters

Understanding how closures capture variables clarifies a whole class of dynamic patterns in Python. When code constructs callables in a loop, the distinction between binding a variable and binding its value is critical. Once this is clear, dynamic APIs based on properties, descriptors, or metaprogramming become predictable, and there is no need to reach for eval.

Takeaways

When dynamically creating properties in a loop, make sure each getter and setter close over a stable value by introducing a dedicated scope, such as a property factory. Keep the data in a separate backing attribute to avoid unintentional recursion. With these two points in place, setattr-based dynamic properties behave consistently and remain easy to maintain.