Properties and Attribute-Boundary Control¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Class Customization Pre Metaclasses"]
page["Properties and Attribute-Boundary Control"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
orient["Orient on the page map"] --> read["Read the main claim and examples"]
read --> inspect["Inspect the related code, proof, or capstone surface"]
inspect --> verify["Run or review the verification path"]
verify --> apply["Apply the idea back to the module and capstone"]
Properties are the friendliest place in the course to say something very important:
attribute access is still runtime machinery, even when the syntax looks like a plain field.
That is why this page matters in Module 06. It keeps one attribute boundary visible without yet escalating into broader descriptor systems.
This page should also correct a common reading mistake: pleasant attribute syntax often makes the owner disappear. A property is valuable precisely because it puts one owner back on the boundary between stored state and public attribute access.
The sentence to keep¶
When you see @property, ask:
what invariant or computation is being owned at this one attribute boundary?
That question keeps properties from being mistaken for ordinary stored values.
Keep a second question beside it:
if the same rule appeared on five fields tomorrow, would this still be the right owner?
That second question is what keeps a property honest. A property is not "small, so it is fine." It is correct only when one attribute boundary is the real surface.
A property is still a descriptor¶
The course has already hinted at this earlier. Here it becomes concrete:
instance.attrtriggers property getter behaviorinstance.attr = valuetriggers property setter behavior when presentdel instance.attrtriggers property deleter behavior when present
So a property is not a special beginner feature separate from descriptor machinery. It is the most approachable descriptor form most Python developers meet.
That is one reason Module 06 places properties before the reusable descriptor pages. Readers need to see that the runtime machinery is already here before the course increases its reuse power.
Begin with the observable conflict¶
Run make class-customization-lab and read property_boundary. Its
DeliveryConfig.endpoint property stores a valid HTTPS URL in _endpoint. The lab then
writes an apparently conflicting entry directly:
Reading config.endpoint still returns "https://example.test", not "shadowed".
That is runtime evidence for data-descriptor precedence. It also reveals the storage
story: the property owns public endpoint; _endpoint owns the value.
Properties are data descriptors¶
This detail matters a lot:
a property is a data descriptor, even when you did not define a custom setter.
That means a property wins over instance dictionary state during lookup and cannot be shadowed the way a non-data descriptor can.
That is one of the most useful lookup corrections Module 06 can make before the descriptor modules arrive.
Use this quick table to keep the lookup consequence concrete:
| Situation | What wins in normal lookup |
|---|---|
property on the class and same-named key in obj.__dict__ |
the property |
| plain instance attribute with no descriptor on the class | the instance attribute |
| non-data descriptor with same-named instance attribute | the instance attribute |
One picture of the lookup boundary¶
obj.x
-> data descriptor on the class?
-> instance storage?
-> non-data descriptor or plain class attribute?
For properties, the first step wins.
That is why a read-only property still beats obj.__dict__["x"] during normal lookup.
That sentence is worth testing directly when you practice. Readers often believe it only after seeing the failed shadowing attempt.
A proof route for property precedence¶
Use this route when you need to prove the lookup story to yourself:
- define a read-only property on the class
- write a same-named key into
obj.__dict__ - read
obj.attr - confirm the property still wins during normal lookup
- explain why direct dictionary inspection is not the same as normal attribute access
That route matters because many learners assume "no setter" means "shadowable." The runtime does not agree.
The packet performs the same conflict with a setter-backed property and records both values. The focused test fails if lookup ever returns the same-named instance entry.
A standard validation example¶
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius must be non-negative")
self._radius = value
This is a good Module 06 example because:
- the invariant belongs to one attribute
- the storage remains explicit
- the class does not need a wider descriptor framework yet
That is exactly the sort of lower-power ownership decision the module is trying to teach.
The lab uses a domain-shaped rule instead: assigning "http://insecure.test" raises
ValueError("endpoint must use https"), and the prior valid endpoint remains visible.
The property therefore owns an invariant on every ordinary assignment, not merely a
formatted read.
It also shows a review habit worth keeping: the public name and the storage name should
tell the same story. If the public boundary is radius, the backing storage should not
turn into a confusing second policy surface.
Use this storage review card when a property starts looking busy:
| Question | Why it matters |
|---|---|
| does the backing name clearly belong to the public attribute? | storage should not become a secret second API |
| does the setter enforce one rule or several unrelated ones? | many policies in one setter usually signal a different owner |
| does the getter compute, normalize, or expose mutable state? | the learner needs to know whether the boundary is over state or behavior |
Read-only does not mean shadowable¶
Even without a custom setter, this property still wins over instance state during lookup.
That is a crucial correction because many people assume "no setter" means the property is like a method-like computed attribute that can be shadowed. It is not.
Do not turn "cannot be shadowed through normal lookup" into "cannot be bypassed." Directly mutating the backing field, overriding lookup, or changing the class can still change behavior. This proof is about descriptor precedence, not tamper-proof state.
Storage patterns to review explicitly¶
Properties often look simple while hiding awkward storage choices. Review these patterns plainly:
| Pattern | Why it matters |
|---|---|
dedicated backing field such as _radius |
keeps the owner story explicit |
| computed getter with no storage | means the attribute is a boundary over behavior, not state |
| setter that normalizes values before storing | makes mutation policy visible at one attribute |
| read-only property with hidden mutable backing object | may still expose mutation through the returned object |
Properties can also be extended in subclasses¶
Another useful pattern is reusing a property from a base class:
class Base:
@property
def value(self):
return 42
class Sub(Base):
@Base.value.getter
def value(self):
return getattr(self, "_value", super().value)
@Base.value.setter
def value(self, value):
self._value = value
This is a good reminder that properties are first-class objects on the class, not only syntax decorations on methods.
It is also a reminder that inheritance can sharpen or blur the owner story. A subclassed property should make the new boundary clearer, not just more surprising.
A debugging route for inherited properties¶
When a property changes in a subclass, review it in this order:
- identify the base-class boundary the original property owned
- say whether the subclass is widening, narrowing, or redirecting that boundary
- confirm the storage story still makes sense after override
- reject the override if the learner would need hidden base-class facts to explain it
Properties are strongest when the boundary is truly one attribute¶
A property is a great fit when:
- one field needs validation
- one computed value needs a read boundary
- one deletion or mutation rule belongs to one name
A property is a weaker fit when:
- many fields need the same behavior
- the design wants reusable field machinery
- the invariant is no longer about one attribute boundary
Those are clues that later descriptor tools may be the more honest owner.
Compare the lab's property_boundary with descriptor_boundary. The property spells
out one HTTPS rule for one attribute. TypedField becomes useful only when the same
shallow type-and-storage rule repeats for endpoint and attempts. Reuse pressure,
not prestige, justifies the stronger owner.
Use this escalation table when you feel unsure:
| Pressure | Stay with property? | Why |
|---|---|---|
| one attribute needs local validation | yes | the rule is still one boundary wide |
| one attribute exposes a computed read view | yes | attribute-shaped access remains honest |
| several attributes copy the same setter policy | no | repetition becomes the real smell |
| the same field rule appears across multiple classes | no | reuse now matters more than one local boundary |
Common overclaims to reject¶
Reject these sentences when they appear in review:
| Overclaim | Better replacement |
|---|---|
| "it's just an attribute" | "it is an attribute-shaped access point with runtime behavior behind it" |
| "read-only means immutable" | "read-only means ordinary assignment is blocked at this boundary" |
| "a property is simpler than a descriptor" | "a property is a focused built-in descriptor for one attribute boundary" |
| "we can keep copying this property pattern" | "repetition may be evidence that a reusable descriptor is the clearer owner" |
Failure modes for property design¶
These are the most common ways a property stops being the clearest owner:
| Failure mode | Why it weakens the design | Repair move |
|---|---|---|
| copied property logic across many fields | the real owner becomes repetition rather than one boundary | promote the repeated rule to a descriptor |
| property returns a mutable object while sounding immutable | readers overtrust the surface | name the mutability limit explicitly |
| backing storage names become inconsistent or magical | the state boundary gets harder to inspect | keep storage explicit and stable |
| property exists where a plain method would be clearer | attribute syntax hides that this is not really attribute-like behavior | switch to a method when no attribute boundary is being protected |
One more failure mode is worth naming:
| Failure mode | Why it weakens the design | Repair move |
|---|---|---|
| calling the property "read-only" while returning a mutable structure without warning | the surface sounds safer than it is | name the mutability limit and decide whether a different API shape would be clearer |
Review rules for properties¶
When reviewing @property usage, keep these questions close:
- what one attribute boundary is this property owning?
- is the storage behind it still explicit and understandable?
- does the review understand that the property is a data descriptor?
- is this still a one-attribute rule, or is the code starting to want reusable field machinery?
- would a plain method be clearer if no true attribute-boundary semantics are needed?
- can another reviewer point to the exact storage or computation owner without guessing?
Evidence packet for property reviews¶
Leave this page with a small packet:
- one lookup trace showing why the property wins over same-named instance state
- one named attribute boundary and its backing-storage story
- one sentence explaining why the rule is still local enough to stay a property
- one rejected escalation note describing what repeated pressure would justify a descriptor
Smallest honest proof route¶
python -m unittest \
tests.test_class_customization_evidence.ClassCustomizationEvidenceTests.test_property_packet_proves_data_descriptor_precedence
This proves normal lookup precedence, assignment failure, and backing-storage separation. It does not prove deep immutability, secrecy, thread safety, or protection against deliberate bypass.
Exit check for this page¶
Before moving on, make sure you can do all of these:
- explain why a property beats same-named instance state during lookup
- name one case where a property is the right owner and one where repetition should push you toward a descriptor
- explain why read-only is weaker than deep immutability
- describe the backing-storage story for one property in plain language
What to practice from this page¶
Try these before moving on:
- Predict both visible values before running the packet.
- Change the valid endpoint and prove a rejected later assignment leaves it unchanged.
- Add a read-only property and show that a same-named entry in
__dict__still loses. - Name the exact repetition that would justify moving this rule into a descriptor.
If those feel ordinary, the next step is to use type hints as declarative aids for shallow validation through descriptors.
Continue through Module 06¶
- Previous: Dataclass Generation Boundaries
- Next: Type Hints and Descriptor-Backed Validation
- Return: Overview
- Terms: Glossary