# Encapsulation, Properties, and Class Variables

> Chapter 15 of the Courseiva PYTHON-PCAP curriculum — https://courseiva.com/learn/python-pcap/encapsulation-properties-and-class-members

**Official objective:** 5.3 — Use private/protected naming, @property decorators, class methods, and static methods.

## Introduction

Without understanding encapsulation and class variables, your code will be a tangled mess where every part of your program can accidentally change the value of a name or an age, causing bugs that are maddening to find. Encapsulation, properties, and class variables are the tools Python gives you to build safe, organised, and predictable code. For your PCAP-31-03 exam, mastering these concepts is not optional; they form the backbone of how professional Python developers write robust applications.

## The Estate Manager Analogy

A senior estate manager in a large country estate is the central figure in charge of a vast property with many critical resources. This manager is responsible for controlling access to the estate's sensitive areas, such as the wine cellar, the private study, and the gardener's tool shed. The manager has a strict rulebook that dictates how different people can interact with these areas. For example, the head gardener has a key to the tool shed but cannot enter the wine cellar. The family lawyer has the code to the study's safe, but only during specific hours. The manager does not give anyone the keys directly. Instead, the manager acts as a gatekeeper when someone needs access: the gardener requests the key, uses the tool shed, and returns the key to the manager. This controlled access protects the estate from misuse and theft.


Furthermore, the manager has a system for tracking the estate's overall resources, like the total amount of heating oil or the main water supply meter. This information belongs to the whole estate, not to any single person or room. The manager reads these global meters regularly and can adjust the heating system centrally. If a tenant asks for the temperature in a specific wing, the manager checks the global thermostat settings, not the local radiator dial.


In this scene, the manager's rulebook and the global resource meters map directly to the Python concepts of encapsulation and class variables. The manager controls how internal data is accessed, and the global resources are shared across the entire estate, not tied to any one room. This is precisely how encapsulation and class variables function in code: they protect data and share state across all instances of a class.

## Core explanation

Let's start with the big problem: what happens when you have a piece of data inside an object that you absolutely must protect from being changed incorrectly? Without protection, any part of your code could accidentally assign an invalid value, like setting a person's age to 'banana' or a bank account balance to a negative number. Encapsulation is the solution to this chaos.


Encapsulation, at its core, is the principle of bundling the data (attributes, also called variables) and the methods (functions) that operate on that data into a single unit, a class, and then restricting direct access to some of that data. Think of it as the protective shell around an object's internal state. The goal is to prevent the internal representation of an object from being directly exposed and modified from outside.


Python achieves this through naming conventions, not strict privacy like some other languages. To make an attribute 'private', you prefix its name with two underscores: `__`. For example, `self.__balance`. This triggers a feature called name mangling. When Python sees this, it internally renames the attribute to `_ClassName__balance`. This makes it harder to accidentally access from outside the class, though it is not completely impossible. A 'protected' attribute uses a single underscore: `_protected_value`. This is a convention that says, "This is internal to the class and its subclasses; please do not touch it from outside."


Where encapsulation really shines is when you combine it with properties. A property is a Python feature that lets you define methods that look like simple attribute access from the outside. You decorate a method with `@property` to make it a getter, and with `@<attribute_name>.setter` to make it a setter. This means you can have a method that runs validation logic every time someone tries to assign a value to an attribute, but the code using your object just does `object.attribute = value`.


Here is how it works step by step:
- You define a private attribute, e.g., `self.__age`.
- You define a method called `age` with the `@property` decorator. This method returns the value of `self.__age`.
- You define another method, also called `age`, but this time decorated with `@age.setter`. Inside this setter, you check if the new value is, say, between 0 and 120. If it is, you set `self.__age = value`. If not, you raise an error or ignore it.


This approach replaces the older, clunkier style of writing explicit `get_age()` and `set_age(value)` methods, which made client code look messy. Properties allow you to start with direct attribute access and later add validation logic without changing any of the code that uses your class. That is a huge advantage.


Now, let us talk about class variables. These are variables that belong to the class itself, not to any single instance (object) of the class. You define them directly inside the class body, outside of any method. For example:


class Employee:
    company_name = "Courseiva"
    employee_count = 0


Every `Employee` object shares the same `company_name`. If you change `Employee.company_name`, every instance sees the change. This is perfect for storing data that should be the same across all instances, like a default value or a running count of how many objects have been created. Instance variables, in contrast, are defined inside `__init__` using `self.attribute = value` and are unique to each object.


Class methods and static methods are the final pieces of this puzzle. A class method is a method that receives the class itself as its first argument (conventionally named `cls`), not an instance. You create it with the `@classmethod` decorator. You use class methods when you need to operate on class-level data or provide an alternative constructor. For example, a class method `from_string(cls, data_string)` could parse a string and return a new instance of the class.


A static method, decorated with `@staticmethod`, does not receive `self` or `cls` as its first argument. It is essentially a regular function that lives inside the class namespace because it logically belongs there. You use it for utility functions that perform a task related to the class but do not need access to any instance or class data, like converting a number from one unit to another.


Together, these tools let you design classes that are self-contained, safe from accidental misuse, and clear in their purpose. You hide the guts of the data (encapsulation), you control how data is read and written (properties), you share data across all objects cleanly (class variables), and you organise functions that belong to the class logically (class and static methods).

## Real-world context

Imagine you are a junior developer at a travel booking company, and your first task is to build a Python class to represent a customer booking. The system will be used by your team and later by a mobile app. You need to ensure that nobody can accidentally set a booking's price to a negative number or assign an invalid date. This is where the concepts of encapsulation and properties become your best friends.


You start by defining a `Booking` class. Your first instinct might be to just use public attributes: `booking.price = 49.99`. But you quickly realise the danger: what if some code later does `booking.price = -100`? That would be a disaster for accounting. So, you use the `@property` decorator to build a controlled interface.


Here is your step-by-step workflow as an IT professional:


- You define a private attribute `self.__price` inside the `__init__` method.
- You write a getter method decorated with `@property` that simply returns the value of `self.__price`.
- You write a setter method decorated with `@price.setter` that checks if the new value is greater than zero. If it is, you assign it. If not, you raise a `ValueError` with a clear message.


This means the accounting team can write `booking.price = 199.99` and it will work, but `booking.price = -50` will immediately throw an error, preventing corrupted data. You have effectively encapsulated the price attribute and added a validation gate.


Next, your manager asks you to track the total number of active bookings across the entire system. You cannot store this in a single booking instance because it needs to be shared. The solution is a class variable. You add:


class Booking:
    total_active_bookings = 0


Then, inside the `__init__` method, you increment it by one: `Booking.total_active_bookings += 1`. When a booking is cancelled, you call a class method that decrements it. Now, any part of the application can check `Booking.total_active_bookings` to get the current count, and it will always be accurate because it is managed by the class itself.


You also need to validate booking reference numbers. You write a static method `validate_reference(ref)` that checks the format of the string (e.g., starts with 'BK-' and has 8 digits). This method does not need any booking data; it just performs a check on a string. You keep it in the `Booking` class because it is logically tied to the booking concept, making the code more organised and easier for other developers to find.


Finally, you add a class method `from_csv_row(cls, row)` that takes a single row of CSV data (a list of values) and creates a new `Booking` object from it. This provides a clean, alternative way to instantiate objects when reading from a file, rather than manually unpacking the data everywhere.


By using encapsulation, properties, class variables, class methods, and static methods, you have built a class that is robust, self-documenting, and resistant to common programming mistakes. Your team can now safely reuse this class without fear of corrupting the booking data. This is exactly what professional Python development looks like in real-world business applications.

## Exam focus

The PCAP-31-03 exam loves to test your understanding of the nuances of encapsulation and the `@property` decorator. They will not just ask you to define the terms; they will give you small code snippets and ask you to predict the output or identify the error. You need to be sharp on the exact behaviour of Python's name mangling and what happens when you use properties incorrectly.


Here are the specific concepts the exam tests again and again:

- Distinguishing between private (__name) and protected (_name) naming. Know that double underscore triggers name mangling, which changes the attribute name internally. Know that a single underscore is purely a convention with no enforced protection.

- The order of getter and setter method definitions. You must define the getter (`@property`) first, then the setter (`@name.setter`). If you define the setter first, Python raises an AttributeError.

- What happens when you try to access a mangled attribute from outside the class. The exam expects you to know that `obj.__attr` will raise an AttributeError unless you use the mangled name `obj._ClassName__attr`.

- Understanding that class variables are shared across all instances. A common trap: the exam shows code where an instance assigns a value to a class variable name using `self.var = value` and asks if the class variable changed. The correct answer is: no, because that assignment creates a new instance attribute that shadows the class variable for that instance.

- The difference between a class method and a static method. The exam tests whether you know that a class method receives `cls` and can access or modify class state, while a static method receives no automatic arguments and cannot access class or instance data.

- Using properties to implement controlled access. Questions will show a `@property` getter and ask you what the setter method's signature must be (it must accept the value to set as the second argument, after `self`).


Common trap patterns:

Trap 1: They give you a class with a private attribute `__value` and a property getter that returns `self.__value`. Then they ask you to assign a new value directly via `obj.__value = 5`. Many beginners think this will trigger the setter. It will not; it will create a new public attribute named `__value` on the instance, leaving the original private attribute unchanged. The property setters only work through the property name, not the mangled name.

Trap 2: They show code that uses `@staticmethod` but then tries to use `self` or `cls` inside the method. This will cause an error because static methods do not receive those arguments. Look for the decorator carefully.

Trap 3: A subclass tries to override a property defined in a parent class. The exam tests whether you know you must use the property decorator again in the subclass or you must use a specific pattern (super() and the property's fget/fset).


Key definitions to memorise for the exam:

- Encapsulation: the bundling of data and methods that operate on that data, restricting direct access to some of an object's components.

- Name mangling: the automatic transformation of `__attribute` to `_ClassName__attribute` in Python.

- Property: a special kind of attribute that allows methods to be accessed like attributes, using the `@property` decorator.

- Class variable: a variable shared by all instances of a class, defined outside any method.

- Instance variable: a variable unique to each instance, defined inside `__init__` with `self`.

- Class method: a method decorated with `@classmethod` that receives the class as its first argument (`cls`).

- Static method: a method decorated with `@staticmethod` that behaves like a regular function but lives in the class namespace.

## Step by step

1. **Define the Class and the Problem** — Create a class, for example `BankAccount`. Identify attributes that need protection, such as `balance`. Without encapsulation, any code could set `balance` to an invalid value like a negative number or a string.
2. **Encapsulate the Sensitive Attribute** — Rename the attribute to `__balance`. This triggers name mangling, making it harder to accidentally access from outside the class. This is the first layer of protection for the internal data.
3. **Create a Property Getter** — Define a method named `balance` and decorate it with `@property`. This method returns the value of `self.__balance`. Now external code can read the balance using `account.balance`, which looks like a simple attribute but is actually a method call.
4. **Create a Property Setter** — Define another method named `balance` decorated with `@balance.setter`. This method accepts a `value` parameter and assigns it to `self.__balance` only if it passes validation, such as checking that the value is a number and is not negative. This prevents invalid data from being stored.
5. **Add a Class Variable for Shared State** — Declare a class variable inside the class body, for example `bank_name = 'Global Trust'`. This variable is shared across all instances. You can also add a counter like `account_count = 0` and increment it inside `__init__` to track how many accounts exist.
6. **Implement a Class Method for Alternative Construction** — Add a `@classmethod` decorated method like `from_initial_deposit(cls, amount)` that creates a new instance with a starting balance. This provides a clear, documented way to create objects for specific use cases, improving code readability.

## Comparisons

### Private Attribute (__attr) vs Protected Attribute (_attr)

**Private Attribute (__attr):**
- Triggers name mangling to _ClassName__attr
- Intended for strictly internal use only
- Harder to accidentally access from outside

**Protected Attribute (_attr):**
- No name mangling, just a naming convention
- Indicates it is internal for subclasses
- Easier to access but still discouraged

### Class Variable vs Instance Variable

**Class Variable:**
- Shared across all instances of the class
- Defined outside any method in the class body
- Changed using ClassName.variable = value

**Instance Variable:**
- Unique to each individual instance
- Defined inside __init__ using self.variable = value
- Changed using self.variable = value

### Property Getter (@property) vs Property Setter (@attr.setter)

**Property Getter (@property):**
- Called when reading the attribute value
- Must be defined before the setter
- Takes only self as an argument

**Property Setter (@attr.setter):**
- Called when assigning a new value
- Defined after the getter
- Takes self and the new value as arguments

### Class Method (@classmethod) vs Static Method (@staticmethod)

**Class Method (@classmethod):**
- Receives the class (cls) as first argument
- Can access and modify class variables
- Used for alternative constructors or factory methods

**Static Method (@staticmethod):**
- Receives no automatic first argument
- Cannot access class or instance data
- Used for utility functions related to the class

### Using a Getter Method (get_balance()) vs Using a Property (obj.balance)

**Using a Getter Method (get_balance()):**
- Requires parentheses: obj.get_balance()
- Breaking change if you refactor the method signature
- More explicit but less elegant syntax

**Using a Property (obj.balance):**
- No parentheses: obj.balance
- Backwards compatible if you add logic later
- Cleaner, more Pythonic interface

## Diagram

_This diagram shows the flow from class definition to private attribute, through property getters and setters, with validation logic protecting the internal state._

```mermaid
flowchart TD
    A[Class Definition] --> B[Instance Variables: self.__data]
    A --> C[Class Variables: shared_data]
    B --> D[Private Attribute: self.__balance]
    D --> E[Property Getter: @property balance]
    D --> F[Property Setter: @balance.setter]
    E --> G[External Code reads using obj.balance]
    F --> H[External Code assigns using obj.balance = new_value]
    H --> I[Validation Logic inside Setter]
    I --> J[If valid: self.__balance = new_value]
    I --> K[If invalid: Raise ValueError]
```

## Common misconceptions

- **Misconception:** Prefixing an attribute with double underscores makes it completely impossible to access from outside the class. **Reality:** Double underscores only trigger name mangling, which renames the attribute to `_ClassName__attr`. It can still be accessed using that mangled name, though it is strongly discouraged. (Beginners hear the word 'private' and assume it means 'inaccessible', like in Java. Python's philosophy is 'we are all consenting adults', so it uses naming conventions rather than enforcement.)
- **Misconception:** A class variable can be changed for a single instance by assigning to it using the instance name. **Reality:** Assigning to a class variable name using `self` on an instance creates a new instance attribute that shadows the class variable. The original class variable remains unchanged for other instances. (This is a subtle difference from how many other languages handle class variables. Beginners think `self.var = 5` modifies the shared state, when it actually creates a local copy for that instance.)
- **Misconception:** Properties are just syntactic sugar; they do not add any real functionality compared to using regular getter/setter methods. **Reality:** Properties allow you to add behaviour (like validation) to attribute access without changing the interface. They also let you start with simple attribute access and later add logic without breaking existing code that uses the class. (The real value of properties is in backwards compatibility and clean API design. Beginners often miss this because they focus on syntax rather than the long-term maintainability benefits.)
- **Misconception:** A static method is basically the same as a class method because both live inside a class. **Reality:** A static method does not receive any automatic first argument (neither `self` nor `cls`), so it cannot access or modify class or instance state. A class method receives `cls` and can access class-level data. (The names are similar and both are decorated methods, so beginners conflate them. The key distinction is what arguments they automatically receive, which changes what they can do.)
- **Misconception:** You must always use private attributes with properties; otherwise, you are doing encapsulation wrong. **Reality:** Encapsulation is a principle, not a strict rule. Many professional Python classes use public attributes when there is no need for validation or computed values. The goal is to hide what can change, but not everything needs hiding. (Beginners often think every attribute must be private and have a getter/setter, leading to over-engineering. The Python community encourages simple, direct access where appropriate.)

## Key takeaways

- Encapsulation bundles data and methods together and restricts direct access to an object's internal state using naming conventions like double underscores for privacy.
- Name mangling transforms `__attribute` into `_ClassName__attribute`, making it harder (but not impossible) to access from outside the class.
- The `@property` decorator allows you to define getter and setter methods that can be accessed like regular attributes, enabling validation and computed values.
- A class variable is shared across all instances of a class and can be used to store default values or track global state, like a count of instances.
- Assigning a value to a class variable name using `self` on an instance creates a new instance attribute that shadows the class variable without modifying it.
- A class method receives the class (`cls`) as its first argument and can access class-level data, while a static method receives no automatic arguments and acts like a regular function inside the class.

## FAQ

**What is the difference between private and protected attributes in Python?**

Private attributes (double underscore prefix) trigger name mangling, which changes the attribute name internally to `_ClassName__attr`, making it harder to access. Protected attributes (single underscore) are just a naming convention that tells other developers 'this is internal, do not use it directly'.

**Can I still access a private attribute from outside the class?**

Yes, you can access it using the mangled name `obj._ClassName__attr`, but doing so violates the intent of encapsulation and is strongly discouraged. The convention is to respect the privacy as if it were enforced.

**What happens if I define the setter before the getter in a property?**

Python will raise an AttributeError because the `@attribute.setter` decorator expects the property object (created by `@property`) to already exist. The getter must always be defined first.

**When should I use a class variable versus an instance variable?**

Use a class variable when the data should be shared across all instances of a class, like a default value, a configuration setting, or a counter of instances. Use an instance variable when the data is unique to a specific object, like a person's name or an account balance.

**What is the difference between a class method and a static method?**

A class method receives the class itself as its first argument (`cls`) and can access or modify class-level data. A static method does not receive any automatic first argument and cannot access class or instance data; it is essentially a regular function placed inside the class for organisation.

**Does Python have true private attributes like Java or C++?**

No, Python uses a convention-based approach. Double underscore prefix triggers name mangling for privacy, but it is not enforced by the interpreter. The philosophy is 'we are all consenting adults', meaning the language trusts developers to respect the conventions.

## Check your understanding

1. **What does Python do when you prefix an attribute with two underscores (__) inside a class?**

   Answer: It triggers name mangling, which internally renames the attribute to `_ClassName__attribute`, making it harder to accidentally access from outside the class.

2. **If you define a setter method with `@price.setter` but do not define a getter method first, what happens?**

   Answer: Python raises an AttributeError because the `@price.setter` decorator requires an existing property object created by `@property`.

3. **What is the result of assigning a value to a class variable using `self.var = 5` inside an instance method?**

   Answer: It creates a new instance attribute named `var` on that specific object, which shadows the class variable `var` for that instance without modifying the shared class variable.

4. **What is the key difference between a class method and a static method regarding the arguments they receive?**

   Answer: A class method receives the class itself as its first argument (conventionally named `cls`), while a static method receives no automatic first argument.

5. **What is the main purpose of the `@property` decorator?**

   Answer: It allows you to define methods that can be accessed like simple attributes, enabling you to add validation, computed values, or logging logic without changing the external interface of the class.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/python-pcap/encapsulation-properties-and-class-members
