Courseiva
PCAP-31-03Chapter 15 of 17Objective 5.3

Encapsulation, Properties, and Class Variables

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.

12 min read
Intermediate
Updated Jul 23, 2026
Editorial oversight: Johnson Ajibi· Security Systems Architect · MSc IT Security · IEEE Senior Member

A simple way to picture Encapsulation, Properties, and Class Variables

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.

How It Actually Works

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).

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

Walk-Through

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.

What This Looks Like on the Job

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.

How PCAP-31-03 Actually Tests This

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.

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.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

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

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)

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)

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())

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

Watch Out for These

Mistake

Prefixing an attribute with double underscores makes it completely impossible to access from outside the class.

Correct

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.

Mistake

A class variable can be changed for a single instance by assigning to it using the instance name.

Correct

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.

Mistake

Properties are just syntactic sugar; they do not add any real functionality compared to using regular getter/setter methods.

Correct

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.

Mistake

A static method is basically the same as a class method because both live inside a class.

Correct

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.

Mistake

You must always use private attributes with properties; otherwise, you are doing encapsulation wrong.

Correct

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.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

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.

Terms Worth Knowing

Keep going

You've finished Encapsulation, Properties, and Class Variables. Continue through the PCAP-31-03 study guide to build a complete picture of the exam.

Done with this chapter?