Skip to main content

Encapsulation and Property Decorators

In Chapter 27, you learned the basics of classes and access control. Now you'll master encapsulation through properties—a technique that protects your data while keeping your API clean and Pythonic. Through task priority validation, you'll discover how properties enforce business rules across Todo apps, invoicing systems, and appointment scheduling. By the end, you'll understand why properties are central to professional OOP design and how the same validation pattern applies everywhere.


Part 1: Experience the Problem and Solution

Your Role: Code explorer discovering why unvalidated attributes create bugs

Discovery Exercise: The Dangerous Unprotected Task

Create unprotected_task.py:

Loading Python environment...

Output:

Task: Review pull request, Priority: 1
After bug - Priority: 999

Your observation:

  • No validation exists—any integer is accepted
  • If priority rules change (e.g., "must be 1-10"), where do you update the logic?
  • Direct attribute access makes it impossible to enforce rules

Discovery Exercise 2: Protected Attribute with Method

Now add validation through a method:

Loading Python environment...

Output:

Task: Review pull request, Priority: 1
Error: Priority must be between 1 and 10
After update - Priority: 3

Your observation:

  • Protected attribute (_priority) signals "don't touch this directly"
  • get_priority() and set_priority() enforce validation in ONE place
  • If rules change, update the setter once
  • BUT: It feels verbose compared to direct attribute access

Discovery Exercise 3: The Pythonic Solution - Properties

Now use @property and @setter decorators:

Loading Python environment...

Output:

Task: Review pull request, Priority: 1
Error: Priority must be between 1 and 10
After update - Priority: 3

Your observation:

  • Looks like attribute access (task.priority = 3)
  • Actually runs validation code in the setter
  • This is the Pythonic way—clean API + hidden validation
  • Compare: task.set_priority(3) vs task.priority = 3 — which feels more natural?

Part 2: Learn Properties From AI Teacher

Your Role: Student receiving explanation from AI Teacher

AI Teaching Prompt

Ask your AI companion:

"I've discovered three ways to handle priority in a Task class:

  1. Direct attribute: self.priority = value (no validation)
  2. Methods: task.set_priority(3) and task.get_priority() (validated)
  3. Properties: task.priority = 3 (validated, looks like attributes)

Explain:

  • Why is the property approach better than method approach?
  • What does @property decorator do? What does @priority.setter do?
  • Why is task.priority = 5 more Pythonic than task.set_priority(5)?
  • Show me how the priority property stops invalid values"

Convergence Activity

After AI explains, ask this follow-up question:

"Now I understand properties hide validation behind attribute-like syntax. Show me a property that's read-only (getter only, no setter). When would I want a property you can't change after creation?"

What you're learning: Properties bridge two worlds—they look like simple attributes from the outside but enforce rules inside. This is the Pythonic philosophy: "make the simple case simple, but allow complexity when needed." The same pattern works for task priority, invoice amounts, appointment times, anywhere you need validation.


Part 3: Challenge AI with Design Decisions

Your Role: Student teaching AI by testing its understanding

Challenge 1: Priority Validation Across Domains

"Build Task, Invoice, and Appointment classes where:

  • Task has priority (1-10)
  • Invoice has discount_percent (0-100)
  • Appointment has duration_minutes (15-480 in 15-min increments)

Use @property/@setter for validation in all three. Show the pattern is identical—validation rules change, but the @property/@setter structure stays the same. Why is this pattern called 'domain-driven validation'?"

Challenge 2: Computed Properties (Read-Only)

"In a Task class with _priority attribute, create a read-only property urgency_label that returns:

  • 'Critical' if priority is 2 or less
  • 'High' if priority is 4 or less
  • 'Medium' if priority is 7 or less
  • 'Low' if priority is greater than 7

Why is this a property instead of a method? When do you use @property without @setter (read-only computed properties)?"

Challenge 3: Type and Value Validation

"Create an Invoice class with amount property. Validate:

  • Must be float or int (convert int to float)
  • Must be positive (> 0)
  • Cannot be less than 0.01 (minimum charge)

Then add a discount property that:

  • Accepts 0-100 percent
  • Won't set if amount hasn't been set yet
  • Raises meaningful error messages

Show how property validation prevents impossible states."

Deliverable

Document the three challenges and AI responses in property_validation_patterns.md. Include your analysis of which patterns transfer across domains.


Part 4: Master Encapsulation Across Domains

Your Role: Knowledge synthesizer building reusable validation patterns

Pattern 1: Simple Range Validation

The most common pattern—enforce minimum and maximum values:

Loading Python environment...

Applied to multiple domains:

Invoice Amount Validation

Loading Python environment...

Output:

Invoice: 1500.0
Error: Amount must be positive
Updated: 2000.5

Appointment Status Validation

Loading Python environment...

Output:

Status: scheduled
Error: Status must be one of {'scheduled', 'in_progress', 'completed', 'cancelled'}. Got 'pending'
Updated: completed

Pattern 2: Computed Properties (Read-Only)

Properties that calculate values from protected attributes without setter:

Loading Python environment...

Output:

Task: Ship release
Days until due: 19
Is overdue? False
After marking complete - Is overdue? False

Pattern 3: Dependent Property Validation

Some properties depend on other properties being set:

Loading Python environment...

Output:

Original: 1000.0
Error: Cannot set discount before amount is set
Discount: 10.0
Final: 900.0

Pattern 4: Type Coercion and Validation

Validate and convert types automatically:

Loading Python environment...

Output:

Priority: 1
After string assignment: 5
Error: Cannot convert 'urgent' to priority number

Encapsulation and Property Design Guide

Create property_pattern_guide.md:

PatternUse WhenExample
@property getter onlyExpose protected attribute read-onlytask.priority - read but protect from modification
@property + @setterNeed validation on both read and writeTask.priority, Invoice.amount must validate on set
Computed property (no setter)Value derives from other attributestask.is_overdue, invoice.final_amount
Type coercion in setterAccept multiple types, convert automaticallyAccept priority = "5" or priority = 5
Dependent validationOne property depends on anotherCan't set discount until amount is set

Pythonic Principles:

  • Use @property instead of get_priority() methods
  • Make simple cases simple: task.priority = 5
  • Hide complexity: validation runs automatically
  • Protect internals: _priority signals "don't touch"
  • Meaningful errors: "Priority must be 1-10", not "Invalid"

Deliverable: Complete guide with decision criteria for each pattern and domain examples.


Try With AI

Ready to master properties, encapsulation, and validation across real-world domains?

Prompt 1: Task Priority Validation with Properties

"Build a Task class where priority is protected with a @property decorator. Validate that priority:

  • Must be an integer
  • Must be between 1 and 10
  • Raises TypeError if not integer
  • Raises ValueError if out of range

Show the implementation, then demonstrate:

Loading Python environment...

Explain why using @property is better than task.set_priority(5) or task.get_priority()."

What you're learning: The @property decorator creates a clean API. From the outside, task.priority looks like a simple attribute. Inside, validation runs automatically. This is Pythonic—the language should adapt to your domain, not the reverse. Priority validation applies everywhere: Task, Invoice, Appointment, Case. You'll use this pattern constantly.


Prompt 2: Invoice Amount Validation with Type Coercion

"Create an Invoice class with amount property that:

  • Accepts int or float
  • Converts int to float
  • Rejects negative, zero, or amounts < 0.01
  • Raises TypeError for non-numeric types
  • Raises ValueError with the specific problem ('Amount must be positive', 'Minimum is 0.01')

Test with:

Loading Python environment...

Apply the same pattern to an Appointment class with duration_minutes property (must be 15-480 in 15-min increments). Show how the validation structure is identical."

What you're learning: Properties enforce business rules consistently. The pattern is always the same: (1) check type, (2) check value constraints, (3) raise specific errors. When you apply this to 5 different classes, you recognize the pattern and code faster. This is how expert developers work—they transfer patterns across domains.


Prompt 3: Computed Properties and Read-Only Access

"Create a Task class with:

  • title, priority, due_date attributes (protected with _)
  • priority property with @setter (validate 1-10)
  • is_overdue computed property (True if due_date < now() and not done)
  • days_until_due computed property (returns int days remaining)
  • urgency_label computed property ('Critical', 'High', 'Medium', 'Low' based on priority)

Show that these computed properties have @property but no @setter (read-only). Why would you try to do task.is_overdue = True? What should happen? Demonstrate the difference between a property you CAN change (priority) vs properties you CANNOT change (is_overdue, urgency_label)."

What you're learning: Properties can be read-only (no setter). Some things should never be assigned directly—overdue status should be calculated, not set by hand. When you see a property without a setter, you know "this value is computed, don't set it." This is another Pythonic principle—the language communicates intent through code structure.


Prompt 4: Dependent Property Validation (Advanced)

"In an Invoice class:

  • amount property (must be positive, min 0.01)
  • discount_percent property (0-100%, but CANNOT be set unless amount > 0)
  • final_amount computed property (returns amount * (1 - discount/100))

Show that:

Loading Python environment...

Why would you create dependent validation? What business scenario requires 'cannot discount a zero-dollar invoice'? Apply this pattern to a Task class where status can't be 'completed' unless priority was set."

What you're learning: Real systems have complex rules. Properties let you enforce them cleanly. Dependent validation prevents invalid states from ever existing in your code. This is called "making impossible states impossible"—if your code logic makes it impossible to create an overdue non-deadline task or a discounted zero-dollar invoice, bugs can't happen.


Try With AI: Mastery Through Application

You've now discovered all the patterns. Time to integrate them with AI and master the complete property validation toolkit.


Synthesis: Multi-Domain Property Validation

"I've learned @property decorators with validation across Task, Invoice, and Appointment classes. Now show me one comprehensive example that combines everything:

Build a Case class (legal domain) with:

  1. case_number property (string, must match format 'CASE-XXXX-YYYY')
  2. status property (enum: 'open', 'discovery', 'trial', 'closed' with validation)
  3. amount_at_stake property (must be positive, cannot be changed once status is 'trial' or 'closed')
  4. is_critical computed property (True if amount > $1M)
  5. days_in_discovery computed property (read-only)

Show how the same patterns from Task, Invoice, and Appointment apply to Case. Why is this pattern called 'domain-driven property design'? Apply this architecture to a Patient class (healthcare domain)."

What you're learning: You're now a professional-grade OOP architect. The property validation pattern is universal—Task priority, Invoice amount, Case status, Patient ID. When you see a new class, you know: (1) Which attributes need validation? (2) Which are protected (_)? (3) Which are computed (no setter)? (4) What are the business rules? Properties let you enforce all of this while keeping the API clean. This is what separates amateur code from professional systems.