Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • šŸ“ Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • šŸ“ Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • šŸ“ Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • šŸ“ Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • šŸ“ While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • šŸ“ For Each Assignments
    • šŸ“ All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • šŸ“ Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • šŸ“ Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • šŸ“ Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • šŸ“ Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • šŸ“ Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • šŸ“ Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • šŸ“ Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • šŸ“ Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • šŸ“ Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • šŸ“ Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • šŸ“ Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • šŸ“ Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • šŸ“ Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • šŸ“ Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • šŸ“ Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • šŸ“ Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • šŸ“ Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • šŸ“ Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • šŸ“ Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Encapsulation

Python Encapsulation - Complete Guide

Learn how to protect and control access to your data.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What is Encapsulation?
  • Why Encapsulate?
  • Access Modifiers in Python
  • Getters and Setters
  • The @property Decorator
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • What is encapsulation — bundling data and methods that work on that data
  • Data hiding — protecting internal state from direct access
  • Access modifiers — public, protected, and private in Python
  • Getters and setters — controlling access to attributes
  • The @property decorator — Python's elegant way to create getters and setters
  • Real-world use — practical examples you can use

What is Encapsulation?

Encapsulation is one of the four pillars of Object-Oriented Programming (along with inheritance, polymorphism, and abstraction). It's about bundling data and the methods that work on that data together, and controlling access to that data from outside the class.

Think of encapsulation like a vending machine. You interact with the machine through a clear interface — you put in money, press buttons, and get your snack. But you don't interact directly with the internal mechanisms — the coin sensors, the motor, the conveyor belt. Those are encapsulated inside the machine.

In Python, encapsulation is implemented through:

  • Access modifiers — public, protected, private conventions
  • Getters and setters — methods that control access to attributes
  • The @property decorator — Python's elegant way to create controlled access

šŸ’” Key concept: Encapsulation is about what you expose to the outside world, not how it works internally. It's about protecting your data and providing a clean interface.

Why Encapsulate?

1

The Benefits of Encapsulation

Encapsulation isn't just a fancy OOP concept — it has real, practical benefits that make your code better.

# Why Encapsulation Matters

print("=" * 50)
print("WHY ENCAPSULATE?")
print("=" * 50)

# ============================================================
# WITHOUT ENCAPSULATION — The Problem
# ============================================================

print("\nāŒ WITHOUT ENCAPSULATION:")

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance  # Public — anyone can change it!

# Everything is public — no protection
account = BankAccount("Alice", 1000)
print(f"Initial balance: ${account.balance}")

# Anyone can directly modify the balance
account.balance = -500  # Negative balance! 😱
print(f"After direct modification: ${account.balance}")

# No validation, no protection, no consistency
account.balance = "abc"  # String instead of number! 😱
print(f"After assigning a string: ${account.balance}")
print("This will cause errors when trying to do math with it!")

print("\nāŒ Problems:")
print("  1. No validation — can set invalid values")
print("  2. No protection — can break the object's state")
print("  3. No consistency — no way to ensure rules are followed")


# ============================================================
# WITH ENCAPSULATION — The Solution
# ============================================================

print("\nāœ… WITH ENCAPSULATION:")

class SecureBankAccount:
    def __init__(self, owner, initial_balance):
        self._owner = owner
        self._balance = 0  # Protected
        self._transaction_history = []
        
        # Use the setter method to validate
        self.deposit(initial_balance)
    
    def deposit(self, amount):
        """Add money to the account with validation"""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        self._transaction_history.append(f"Deposited: +${amount}")
        return f"Deposited ${amount}. New balance: ${self._balance}"
    
    def withdraw(self, amount):
        """Withdraw money with validation"""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise ValueError(f"Insufficient funds. Balance: ${self._balance}")
        self._balance -= amount
        self._transaction_history.append(f"Withdrew: -${amount}")
        return f"Withdrew ${amount}. New balance: ${self._balance}"
    
    def get_balance(self):
        """Get balance — read-only access"""
        return self._balance
    
    def get_owner(self):
        """Get owner — read-only access"""
        return self._owner
    
    def get_transactions(self):
        """Get transaction history — returns a copy to prevent modification"""
        return self._transaction_history.copy()

# Using the encapsulated class
secure_account = SecureBankAccount("Bob", 1000)
print(f"Owner: {secure_account.get_owner()}")
print(f"Balance: ${secure_account.get_balance()}")

print(f"\n{secure_account.deposit(500)}")
print(f"{secure_account.withdraw(200)}")

# Can't directly access or modify the balance
# secure_account._balance = -500  # Would work but is discouraged
# print(secure_account._balance)  # Accessible but not recommended

print("\nāœ… Benefits of Encapsulation:")
print("  1. Validation — prevents invalid data")
print("  2. Protection — controls how data can be changed")
print("  3. Consistency — ensures data always follows rules")
print("  4. Maintainability — can change internal logic without breaking external code")
print("  5. Clear interface — users know exactly what methods to call")

Benefits of encapsulation:

  • Data protection — prevents invalid or inconsistent data
  • Validation — ensures data meets requirements before being stored
  • Maintainability — internal changes don't affect external code
  • Clear interface — users know what methods to call
  • Reduces bugs — prevents accidental misuse of data

Quick Check: What's the main benefit of encapsulation? (Answer: It protects data and ensures it remains valid and consistent)

Access Modifiers in Python

2

Public, Protected, and Private

Python uses naming conventions to indicate the intended visibility of class members:

  • Public — name (no underscore) — accessible from anywhere
  • Protected — _name (single underscore) — intended for internal use
  • Private — __name (double underscore) — name mangled, harder to access

Remember: Python's access modifiers are conventions, not strict enforcement. Python trusts developers to respect the conventions.

# Access Modifiers in Python

print("=" * 50)
print("ACCESS MODIFIERS IN PYTHON")
print("=" * 50)

class AccessDemo:
    """Demonstrates public, protected, and private members"""
    
    def __init__(self):
        # Public — no underscore, accessible everywhere
        self.public = "I'm public"
        
        # Protected — single underscore, for internal use
        self._protected = "I'm protected"
        
        # Private — double underscore, name mangled
        self.__private = "I'm private"
    
    def public_method(self):
        """Public method — accessible everywhere"""
        return "Public method called"
    
    def _protected_method(self):
        """Protected method — for internal use"""
        return "Protected method called"
    
    def __private_method(self):
        """Private method — name mangled"""
        return "Private method called"
    
    def access_all_inside(self):
        """Inside the class — all are accessible"""
        return {
            "public": self.public,
            "protected": self._protected,
            "private": self.__private,
            "public_method": self.public_method(),
            "protected_method": self._protected_method(),
            "private_method": self.__private_method()
        }

# ============================================================
# DEMONSTRATION
# ============================================================

demo = AccessDemo()

print("\n1. INSIDE THE CLASS — ALL ACCESSIBLE")
inside = demo.access_all_inside()
for key, value in inside.items():
    print(f"   {key}: {value}")

print("\n2. OUTSIDE THE CLASS — VISIBILITY")
print(f"   Public: {demo.public}")
print(f"   Public method: {demo.public_method()}")

# Protected — accessible but discouraged
print(f"   Protected: {demo._protected}")
print(f"   Protected method: {demo._protected_method()}")

# Private — NOT directly accessible
try:
    print(demo.__private)
except AttributeError as e:
    print(f"   āŒ Private: {e}")

try:
    print(demo.__private_method())
except AttributeError as e:
    print(f"   āŒ Private method: {e}")

print("\n3. NAME MANGLING — PRIVATE ACCESS (HACK)")
print(f"   Mangled private: {demo._AccessDemo__private}")
print(f"   Mangled private method: {demo._AccessDemo__private_method()}")

print("\n" + "-" * 30)
print("ACCESS MODIFIERS SUMMARY")
print("-" * 30)
print("""
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Modifier        │ Syntax           │ Visibility                         │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Public          │ name             │ Accessible everywhere              │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Protected       │ _name            │ For internal use (convention)      │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Private         │ __name           │ Name mangled (harder to access)    │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

šŸ“Œ Remember: These are CONVENTIONS, not strict enforcement.
   Python trusts developers to "be consenting adults."
""")

Access modifiers key points:

  • Public — no underscore, intended for external use
  • Protected — single underscore, for internal use (subclasses can access)
  • Private — double underscore, name mangling prevents accidental access
  • Conventions, not enforcement — Python trusts developers
  • Name mangling — __name becomes _ClassName__name

Quick Check: How do you indicate a private member in Python? (Answer: Use a double underscore prefix — __name)

Getters and Setters

3

Controlled Access to Attributes

Getters and setters are methods that control access to an object's attributes. They let you add logic — like validation, logging, or transformation — when someone gets or sets a value.

In many languages, getters and setters are written explicitly. In Python, we often use the @property decorator instead, but it's helpful to understand the traditional approach first.

# Traditional Getters and Setters

print("=" * 50)
print("GETTERS AND SETTERS")
print("=" * 50)

class Person:
    """Person with traditional getters and setters"""
    
    def __init__(self, name, age):
        self._name = name
        self._age = age
    
    # ----- GETTERS -----
    def get_name(self):
        """Get the person's name"""
        return self._name
    
    def get_age(self):
        """Get the person's age"""
        return self._age
    
    # ----- SETTERS -----
    def set_name(self, name):
        """Set the person's name with validation"""
        if not name or not name.strip():
            raise ValueError("Name cannot be empty")
        self._name = name.strip()
        return f"Name updated to: {self._name}"
    
    def set_age(self, age):
        """Set the person's age with validation"""
        if not isinstance(age, int):
            raise TypeError("Age must be an integer")
        if age < 0:
            raise ValueError("Age cannot be negative")
        if age > 150:
            raise ValueError("Age must be realistic (under 150)")
        self._age = age
        return f"Age updated to: {self._age}"
    
    # ----- OTHER METHODS -----
    def get_info(self):
        """Get complete info"""
        return f"Name: {self._name}, Age: {self._age}"

# ============================================================
# DEMONSTRATION
# ============================================================

person = Person("Alice", 30)

print("1. GETTING VALUES")
print(f"   Name: {person.get_name()}")
print(f"   Age: {person.get_age()}")
print(f"   Info: {person.get_info()}")

print("\n2. SETTING VALUES (VALID)")
print(f"   {person.set_name('Bob')}")
print(f"   {person.set_age(35)}")
print(f"   Info: {person.get_info()}")

print("\n3. SETTING VALUES (INVALID)")
try:
    person.set_age(-5)
except ValueError as e:
    print(f"   āŒ {e}")

try:
    person.set_name("")
except ValueError as e:
    print(f"   āŒ {e}")

try:
    person.set_age("thirty")
except TypeError as e:
    print(f"   āŒ {e}")

print("\n" + "-" * 30)
print("GETTERS AND SETTERS — PROS AND CONS")
print("-" * 30)
print("""
āœ… PROS:
   • Full control over attribute access
   • Can add validation and business logic
   • Can change internal representation without breaking external code
   • Can add logging, caching, or other cross-cutting concerns

āŒ CONS:
   • Verbose — lots of boilerplate code
   • Less Pythonic — other Python developers expect properties
   • Can make code harder to read
   • The @property decorator is usually a better choice
""")

Getters and setters key points:

  • Getters — methods that retrieve attribute values
  • Setters — methods that set attribute values with validation
  • Control — add logic like validation, logging, transformation
  • Encapsulation — hide internal representation
  • Verbose — can be a lot of code for simple cases

Quick Check: What's the main advantage of using getters and setters? (Answer: They allow you to add validation and logic when accessing or modifying attributes)

The @property Decorator

4

Python's Elegant Solution

The @property decorator is Python's elegant way to create getters and setters. It lets you define methods that can be accessed like attributes — without the need for explicit getter/setter method calls.

This is the Pythonic way to implement encapsulation. It gives you the best of both worlds: simple attribute-like syntax with full control over access.

# The @property Decorator — Pythonic Encapsulation

print("=" * 50)
print("THE @property DECORATOR")
print("=" * 50)

class Temperature:
    """Temperature with property-based encapsulation"""
    
    def __init__(self, celsius):
        self._celsius = celsius
        self._fahrenheit = celsius * 9/5 + 32
    
    # ----- PROPERTY GETTER -----
    @property
    def celsius(self):
        """Get temperature in Celsius"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """Set temperature in Celsius with validation"""
        if not isinstance(value, (int, float)):
            raise TypeError("Temperature must be a number")
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero (-273.15°C)")
        self._celsius = value
        self._fahrenheit = value * 9/5 + 32
    
    @property
    def fahrenheit(self):
        """Get temperature in Fahrenheit"""
        return self._fahrenheit
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Set temperature in Fahrenheit with validation"""
        if not isinstance(value, (int, float)):
            raise TypeError("Temperature must be a number")
        celsius = (value - 32) * 5/9
        if celsius < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = celsius
        self._fahrenheit = value
    
    @property
    def kelvin(self):
        """Get temperature in Kelvin (read-only property)"""
        return self._celsius + 273.15
    
    def __repr__(self):
        return f"Temperature({self._celsius}°C / {self._fahrenheit}°F)"

# ============================================================
# DEMONSTRATION
# ============================================================

temp = Temperature(25)

print("1. GETTING VALUES (like attributes)")
print(f"   Celsius: {temp.celsius}°C")
print(f"   Fahrenheit: {temp.fahrenheit}°F")
print(f"   Kelvin: {temp.kelvin}K")
print(f"   {temp}")

print("\n2. SETTING VALUES (with validation)")
temp.celsius = 30
print(f"   After setting celsius=30: {temp}")
print(f"   Fahrenheit automatically updated: {temp.fahrenheit}°F")

temp.fahrenheit = 100
print(f"   After setting fahrenheit=100: {temp}")
print(f"   Celsius automatically updated: {temp.celsius}°C")

print("\n3. TRYING INVALID VALUES")
try:
    temp.celsius = -300
except ValueError as e:
    print(f"   āŒ {e}")

try:
    temp.celsius = "hot"
except TypeError as e:
    print(f"   āŒ {e}")

print("\n4. READ-ONLY PROPERTY")
print(f"   Kelvin (read-only): {temp.kelvin}K")
try:
    temp.kelvin = 300
except AttributeError as e:
    print(f"   āŒ Cannot set kelvin: {e}")

print("\n" + "-" * 30)
print("@property BENEFITS:")
print("-" * 30)
print("""
āœ… Clean syntax: access like attributes (obj.attribute)
āœ… Encapsulation: control access with getters/setters
āœ… Validation: add rules when setting values
āœ… Read-only: properties without setters are read-only
āœ… Computed values: calculate on the fly
āœ… Pythonic: this is the Python way to do encapsulation

šŸ“Œ The @property decorator is the recommended way to
   implement encapsulation in Python.
""")

@property key points:

  • Clean syntax — access like attributes, not methods
  • Validation — add logic in the setter
  • Read-only — define property without a setter
  • Computed values — calculate values on the fly
  • Pythonic — the preferred way to implement encapsulation

Quick Check: What decorator is used to create Pythonic getters and setters? (Answer: @property)

Real-World Example

5

Building a User Management System

# Real-World Example: User Management System

import re
import hashlib
import secrets
from datetime import datetime

print("=" * 60)
print("USER MANAGEMENT SYSTEM")
print("=" * 60)

class User:
    """Secure user management with encapsulation"""
    
    def __init__(self, username, email, password):
        self._username = None
        self._email = None
        self._password_hash = None
        self._created_at = datetime.now()
        self._last_login = None
        self._is_active = True
        self._failed_attempts = 0
        self._session_token = None
        
        # Use setters for validation
        self.username = username
        self.email = email
        self.password = password
    
    # ----- PROPERTIES -----
    
    @property
    def username(self):
        """Get username (read-only once set)"""
        return self._username
    
    @username.setter
    def username(self, value):
        """Set username with validation"""
        if not value or not value.strip():
            raise ValueError("Username cannot be empty")
        if len(value) < 3:
            raise ValueError("Username must be at least 3 characters")
        if not re.match(r'^[a-zA-Z0-9_]+$', value):
            raise ValueError("Username can only contain letters, numbers, and underscores")
        if self._username is not None:
            raise ValueError("Username cannot be changed after creation")
        self._username = value.strip()
    
    @property
    def email(self):
        """Get email"""
        return self._email
    
    @email.setter
    def email(self, value):
        """Set email with validation"""
        if not value or not value.strip():
            raise ValueError("Email cannot be empty")
        # Simple email validation
        if '@' not in value or '.' not in value:
            raise ValueError("Invalid email format")
        if self._email is not None:
            raise ValueError("Email cannot be changed after creation")
        self._email = value.strip()
    
    @property
    def password(self):
        """Password getter (not allowed)"""
        raise AttributeError("Password is not accessible for security reasons")
    
    @password.setter
    def password(self, value):
        """Set password with hashing"""
        if not value or len(value) < 8:
            raise ValueError("Password must be at least 8 characters")
        # Hash the password
        salt = secrets.token_hex(16)
        self._password_hash = hashlib.sha256((value + salt).encode()).hexdigest()
    
    @property
    def created_at(self):
        """Get creation time (read-only)"""
        return self._created_at
    
    @property
    def last_login(self):
        """Get last login time (read-only)"""
        return self._last_login
    
    @property
    def is_active(self):
        """Check if account is active"""
        return self._is_active
    
    @property
    def session_token(self):
        """Get session token"""
        return self._session_token
    
    # ----- PUBLIC METHODS -----
    
    def login(self, password):
        """Authenticate user"""
        if not self._is_active:
            return "Account is locked. Contact support."
        
        # Check password (simplified)
        # In real code, you'd verify the hash properly
        if len(password) >= 8:
            self._failed_attempts = 0
            self._last_login = datetime.now()
            self._session_token = secrets.token_urlsafe(32)
            return f"Welcome back, {self._username}!"
        else:
            self._failed_attempts += 1
            if self._failed_attempts >= 3:
                self._is_active = False
                return "Account locked due to too many failed attempts."
            return f"Invalid password. {3 - self._failed_attempts} attempts remaining."
    
    def logout(self):
        """Log out user"""
        self._session_token = None
        return "Logged out successfully."
    
    def change_password(self, old_password, new_password):
        """Change password with verification"""
        # Verify old password (simplified)
        if len(old_password) < 8:
            return "Invalid current password."
        
        if len(new_password) < 8:
            return "New password must be at least 8 characters."
        
        self.password = new_password
        self._session_token = None
        return "Password changed successfully."
    
    def get_profile(self):
        """Get user profile (read-only public view)"""
        return {
            "username": self._username,
            "email": self._email,
            "created_at": self._created_at.strftime("%Y-%m-%d %H:%M"),
            "last_login": self._last_login.strftime("%Y-%m-%d %H:%M") if self._last_login else "Never",
            "is_active": self._is_active
        }

# ============================================================
# DEMONSTRATION
# ============================================================

print("\n1. CREATING A USER (with validation)")
try:
    user = User("alice123", "alice@example.com", "SecurePass123")
    print("   āœ… User created successfully!")
    print(f"   Username: {user.username}")
    print(f"   Email: {user.email}")
    print(f"   Created: {user.created_at.strftime('%Y-%m-%d %H:%M')}")
except ValueError as e:
    print(f"   āŒ Error: {e}")

print("\n2. TRYING INVALID USERNAME")
try:
    user2 = User("ab", "bob@example.com", "Password123")
except ValueError as e:
    print(f"   āŒ {e}")

print("\n3. TRYING INVALID EMAIL")
try:
    user3 = User("bob123", "invalid-email", "Password123")
except ValueError as e:
    print(f"   āŒ {e}")

print("\n4. TRYING WEAK PASSWORD")
try:
    user4 = User("charlie", "charlie@example.com", "123")
except ValueError as e:
    print(f"   āŒ {e}")

print("\n5. LOGIN ATTEMPTS")
print(f"   {user.login('wrong_password')}")
print(f"   {user.login('wrong_password')}")
print(f"   {user.login('SecurePass123')}")  # Success!

print("\n6. GETTING PROFILE")
profile = user.get_profile()
print(f"   Profile: {profile}")

print("\n7. ACCESSING PASSWORD (Blocked)")
try:
    print(user.password)
except AttributeError as e:
    print(f"   āŒ {e}")

print("\n8. TRYING TO CHANGE USERNAME (Blocked)")
try:
    user.username = "new_alice"
except ValueError as e:
    print(f"   āŒ {e}")

print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("āœ… Properties provide clean, attribute-like access")
print("āœ… Validation ensures data integrity")
print("āœ… Sensitive data (password) is protected")
print("āœ… Read-only properties expose what's needed")
print("āœ… Internal state is hidden from outside")

Real-world example key points:

  • Validation — username, email, and password all have validation
  • Read-only properties — created_at, last_login, session_token
  • Protected data — password is hashed and not accessible
  • Clear interface — users know what methods to call
  • Security — failed attempts tracking, account locking

Quick Check: How is the password protected in this example? (Answer: It's hashed, and there's no getter — attempting to access it raises an AttributeError)

Best Practices

6

Using Encapsulation Effectively

# Best Practices for Encapsulation

print("=" * 60)
print("BEST PRACTICES FOR ENCAPSULATION")
print("=" * 60)

# ============================================================
# 1. USE @property FOR CONTROLLED ACCESS
# ============================================================

print("\n1. USE @property FOR CONTROLLED ACCESS")

class Product:
    """Product with property-based encapsulation"""
    
    def __init__(self, name, price):
        self._name = name
        self._price = price
        self._discount = 0
    
    @property
    def name(self):
        return self._name
    
    @property
    def price(self):
        """Price with discount applied"""
        return self._price * (1 - self._discount / 100)
    
    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value
    
    @property
    def discount(self):
        return self._discount
    
    @discount.setter
    def discount(self, value):
        if not 0 <= value <= 100:
            raise ValueError("Discount must be between 0 and 100")
        self._discount = value

print("   āœ… Use @property for attribute-like access with control")


# ============================================================
# 2. MAKE ATTRIBUTES PRIVATE UNLESS NEEDED
# ============================================================

print("\n2. MAKE ATTRIBUTES PRIVATE UNLESS NEEDED")

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # Protected
        self.__account_number = self._generate_account_number()  # Private
        self._transactions = []  # Protected
    
    def _generate_account_number(self):
        import random
        return f"ACC-{random.randint(10000, 99999)}"
    
    @property
    def balance(self):
        return self._balance
    
    def deposit(self, amount):
        self._balance += amount
        self._transactions.append(f"+{amount}")
    
    def withdraw(self, amount):
        if amount <= self._balance:
            self._balance -= amount
            self._transactions.append(f"-{amount}")

print("   āœ… Use protected (_) for internal attributes")
print("   āœ… Use private (__) for sensitive data")


# ============================================================
# 3. VALIDATE IN SETTERS
# ============================================================

print("\n3. VALIDATE IN SETTERS")

class Student:
    def __init__(self, name, grade):
        self._name = name
        self._grade = grade
    
    @property
    def grade(self):
        return self._grade
    
    @grade.setter
    def grade(self, value):
        if not 0 <= value <= 100:
            raise ValueError("Grade must be between 0 and 100")
        self._grade = value

try:
    s = Student("Alice", 85)
    print(f"   Grade: {s.grade}")
    s.grade = 95  # Valid
    print(f"   Updated grade: {s.grade}")
    s.grade = 150  # Invalid
except ValueError as e:
    print(f"   āŒ {e}")


# ============================================================
# 4. USE READ-ONLY PROPERTIES FOR COMPUTED VALUES
# ============================================================

print("\n4. USE READ-ONLY PROPERTIES FOR COMPUTED VALUES")

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    @property
    def area(self):
        """Computed property (read-only)"""
        return self._width * self._height
    
    @property
    def perimeter(self):
        """Computed property (read-only)"""
        return 2 * (self._width + self._height)

rect = Rectangle(5, 3)
print(f"   Area: {rect.area}")
print(f"   Perimeter: {rect.perimeter}")


# ============================================================
# 5. DON'T OVER-ENCAPSULATE
# ============================================================

print("\n5. DON'T OVER-ENCAPSULATE")

# āœ… DO: Encapsulate when you need control
class Good:
    def __init__(self, value):
        self._value = value
    
    @property
    def value(self):
        return self._value

# āŒ DON'T: Create properties for everything
class OverEncapsulated:
    def __init__(self, x, y, z):
        self._x = x
        self._y = y
        self._z = z
    
    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = value
    
    @property
    def y(self):
        return self._y
    
    @y.setter
    def y(self, value):
        self._y = value
    
    @property
    def z(self):
        return self._z
    
    @z.setter
    def z(self, value):
        self._z = value

# This is unnecessary — just use public attributes!


# ============================================================
# 6. SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("ENCAPSULATION BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ PRACTICE                    │ WHY IT MATTERS                             │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Use @property               │ Clean, Pythonic way to control access      │
│                             │                                             │
│ Make attributes private     │ Protects internal state                     │
│ when possible               │                                             │
│                             │                                             │
│ Validate in setters         │ Ensures data integrity                     │
│                             │                                             │
│ Use read-only properties    │ Expose computed values safely              │
│ for computed values         │                                             │
│                             │                                             │
│ Don't over-encapsulate      │ Simplicity is better than complexity       │
│                             │                                             │
│ Use public attributes       │ Sometimes simplicity is the best choice    │
│ when no control needed      │                                             │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

šŸ“Œ REMEMBER:
   • Encapsulation is about controlling access, not hiding everything
   • Use the simplest approach that meets your needs
   • @property is the Pythonic way to encapsulate
   • Don't write getters and setters for every attribute
""")

Best practices summary:

  • Use @property — it's the Pythonic way to implement encapsulation
  • Make attributes private — use protected/private conventions when needed
  • Validate in setters — ensure data integrity
  • Use read-only properties — for computed values
  • Don't over-encapsulate — use public attributes when no control is needed
  • Keep it simple — simplicity is a core Python principle

Quick Check: What's the Pythonic way to implement getters and setters? (Answer: Use the @property decorator)

Try It Yourself

Experiment with encapsulation in the editor below.

Loading Pyodide... 0%
Python Code Editor
==================================================
ENCAPSULATION - PRACTICE
==================================================

1. BASIC ENCAPSULATION
Name: Alice
Salary: $50,000
Performance: average
Bonus: $2,500

2. SETTING VALUES WITH VALIDATION
Updated salary: $60,000
Updated bonus: $12,000
āŒ Salary cannot be negative
āŒ Performance must be one of: ['excellent', 'good', 'average', 'below average', 'poor']

2. READ-ONLY PROPERTIES
Radius: 5
Area: 78.54
Circumference: 31.42
āŒ Cannot set area: can't set attribute
šŸ†

You've Got It!

You now understand encapsulation in Python. You know how to use access modifiers, getters and setters, and the @property decorator to protect your data and create clean interfaces.

Quick Quiz

Test what you've learned:

1. What is encapsulation in Python?
2. How do you indicate a protected member in Python?
3. What decorator is used for Pythonic getters and setters?
4. What is name mangling?
5. Why should you validate in setters?

Frequently Asked Questions

What is encapsulation in Python? ā–¼

Encapsulation is the practice of bundling data and methods that work on that data together, and controlling access to that data from outside the class. It's implemented using access modifiers and the @property decorator in Python.

What's the difference between protected and private in Python? ā–¼

Protected (_name) is a convention that says "this is for internal use." It's accessible from outside but you shouldn't use it. Private (__name) uses name mangling and is harder to access from outside. Private is for implementation details you really want to hide.

Should I always use @property for attributes? ā–¼

No. Use @property when you need control over access — like validation, computed values, or read-only access. If you don't need any control, just use a public attribute. Over-encapsulating can make your code more complex than necessary.

How do I create a read-only property? ā–¼

Define a property with a getter but without a setter. This makes the property read-only. For example: @property def name(self): return self._name with no @name.setter method.

Why doesn't Python have strict access control like Java? ā–¼

Python follows the philosophy of "We're all consenting adults." It trusts developers to respect naming conventions rather than enforcing access rules. This gives developers more flexibility and trust, which is a core part of Python's design philosophy.

What's the relationship between encapsulation and data hiding? ā–¼

Data hiding is a technique used to implement encapsulation. It means making internal data private (or protected) and only exposing it through controlled interfaces like getters and setters. Encapsulation is the broader principle; data hiding is one way to achieve it.

Where to Go From Here

Now that you understand encapsulation in Python, check out these related topics:

Public, Private and Protected

Learn more about access modifiers in Python.

Learn More →

Inheritance vs Composition

Learn how encapsulation relates to inheritance and composition.

Learn More →

Polymorphism

Learn how encapsulation enables polymorphic behavior.

Learn More →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Inheritance vs Composition Next: Polymorphism →