Encapsulation is a fundamental object-oriented principle in Python. It protects your classes from accidental changes or deletions and promotes code reusability and maintainability. Consider this simple class definition:
class Smartphone:
def __init__(self, brand, os):
self.brand = brand
self.os = os
iphone = Smartphone("Apple", "iOS 17")
Many Python programmers define classes like this. However, it is far from the best practices that pro Pythonistas follow. The problem with this class is evident when you try to modify its data:
iphone.os = "Android"
print(iphone.os)
Imagine an iPhone running on Android — what an outrage! Clearly, we need to set some boundaries within our class so that users can’t change its attributes to whatever they want.
If they do decide to change them, the changes must be under our terms, following our rules. But we still want the .os or .brand attributes to stay the same on the surface.
All of these things can be promoted using encapsulation and in this tutorial, we will learn all about it. Let’s get started!
How is Encapsulation Achieved in Python?
Python supports encapsulation through conventions and programming practices rather than enforced access modifiers. You see, the fundamental principle behind much of Python code design is “We are all adults here.” We can only implement encapsulation as a mere convention and expect other Python developers to trust and respect our code.
In other OOP languages such as Java and C++, encapsulation is strictly enforced with access modifiers such as public, private or protected, but Python doesn't have those:
| Feature | Python | Java | C++ |
|---|
| Access modifiers | No enforced modifiers; uses naming conventions (_protected, __private) | Enforced with public, protected, private keywords | Enforced with public, protected, private keywords |
| Getter/setter methods | Optional, often used with @property decorator for controlled access | Common practice, typically implemented as methods | Common practice, typically implemented as methods |
| Data Access | Accessible via naming conventions; relies on developer discipline | Controlled by access modifiers; enforced by the compiler | Controlled by access modifiers; enforced by the compiler |
| Philosophy | "We are all adults here" - relies on conventions and trust | Strict enforcement of access control | Strict enforcement of access control |
So, most, if not all, of the encapsulation techniques I am about to show you are Python conventions. They can easily be broken if you decide. But I trust that you respect and follow them in your own development projects.
Access modifiers in Python
Let’s say we have this simple class:
class Tree:
def __init__(self, height):
self.height = height
pine = Tree(20)
print(pine.height)
It has a single height attribute that we can print. The problem is that we can also change it to whatever we want:
pine.height = 50
pine.height
pine.height = "Grandma"
pine.height
So, how do we tell users that changing height is off-limits? Well, we could turn it into a protected member by adding a single preceding underscore:
class Tree:
def __init__(self, height):
self._height = height
pine = Tree(20)
pine._height
Now, people who are aware of this convention will know that they can only access the attribute and that we are strongly discouraging them from using and modifying it. But if they want, they can modify it, oh yes.
So, how do we prevent that too? By using another convention — turn the attribute into a private member by adding double preceding underscores:
class Tree:
def __init__(self, height):
self.__height = height
pine = Tree(20)
pine.__height
AttributeError: 'Tree' object has no attribute '__height'
Now, Python will raise an error if someone tries to access the attribute, let alone modify it.
But do you notice what we just did? We hid the only information related to our objects from users. Our class just became useless because it has no public attributes.
So, how do we expose tree height to users but still control how they are accessed and modified? For example, we want tree heights to be within a specific range and only have integer values. How do we enforce that?
At this point, your Java-using friend might chime in and suggest using getter and setter methods. So, let’s try that first:
class Tree:
def __init__(self, height):
self.__height = height
def get_height(self):
return self.__height
def set_height(self, new_height):
if not isinstance(new_height, int):
raise TypeError("Tree height must be an integer")
if 0 < new_height <= 40:
self.__height = new_height
else:
raise ValueError("Invalid height for a pine tree")
pine = Tree(20)
pine.get_height()
In this way, you create a private attribute __height but let users access and modify it in a controlled way using get_height and set_height methods.
pine.set_height(25)
pine.get_height()
Before setting a new value, set_height ensures that the new height is within a certain range and numeric.
pine.set_height("Password")
TypeError: Tree height must be an integer
But these methods seem like overkill for a simple operation. Besides, it is ugly to write code like this:
pine.set_height(pine.get_height() + 5)
Wouldn’t it be more beautiful and readable if we could write this code:
and still enforce the correct data type and range for height? The answer is yes and we will learn how to do just that in the next section.
Using @property decorator in Python classes
We introduce a new technique — creating properties for attributes:
class Tree:
def __init__(self, height):
self.__height = height
@property
def height(self):
return self.__height
pine = Tree(17)
pine.height
We want users to access a hidden attribute named __height as if it were a normal attribute called height. To achieve this, we define a method named height that returns self.__height and decorate it with @property.
Now, we can call height and access the private attribute:
But the best part is that users can’t modify it:
AttributeError: can't set attribute 'height'
So, we add another method called height(self, new_height) that is wrapped by a height.setter decorator. Inside this method, we implement the logic that enforces the desired data type and range for height:
class Tree:
def __init__(self, height):
self.__height = height
@property
def height(self):
return self.__height
@height.setter
def height(self, new_height):
if not isinstance(new_height, int):
raise TypeError("Tree height must be an integer")
if 0 < new_height <= 40:
self.__height = new_height
else:
raise ValueError("Invalid height for a pine tree")
Now, when a user tries to modify the height attribute, @height.setter is called, thus ensuring the correct value is passed:
pine = Tree(10)
pine.height = 33
pine.height = 45
ValueError: Invalid height for a pine tree
We can also customize how the height attribute is accessed through dot-notation with @height.getter:
class Tree:
def __init__(self, height):
self.__height = height
@property
def height(self):
return self.__height
@height.getter
def height(self):
return f"This tree is {self.__height} meters"
pine = Tree(33)
pine.height
Even though we created pine with an integer height, we could modify its value with @height.getter.
These were examples of how we could promote encapsulation in a Python class. Remember, encapsulation is still a convention because we can still break the internal __height private member:
pine._Tree__height = "Gotcha!"
pine.height
'This tree is Gotcha! meters'
Everything in Python classes is public, and so are private methods. It isn’t a design flaw but an instance of the “We are all adults here” approach.
Here's an overview of the Python access modifiers we just reviewed:
| Modifier/Convention | Description | Example |
|---|
| Public | Default access level; attributes and methods are accessible from outside the class | self.attribute |
| Protected | Indicated by a single underscore; a convention to denote partially restricted access | self._attribute |
| Private | Indicated by double underscores; name mangling provides limited access restriction | self.__attribute |
| Properties | Provides controlled access to private attributes using the @property decorator | @property def attribute(self): |
| Read-only Properties | No @attribute.setter; allows read-only access to an attribute | @property def attribute(self): |
Best Practices When Implementing Encapsulation
There are a number of best practices you can follow to ensure that your code aligns well with code written by other experienced OOPistas:
- Create protected or private attributes or methods if they are used only by you. Protected or private members get excluded from documentation and signal others that they can be changed by you, the developer, without any notice, thus discouraging them from using them.
- You don’t always have to create properties for every single class attribute. For large classes with many attributes, writing
attr.getter and attr.setter methods can turn into a headache.
- Consider raising a warning every time a user accesses a protected member (
_).
- Use private members sparingly, as they can make code unreadable for those unfamiliar with the convention.
- Prioritize clarity over obscurity. As encapsulation aims to improve code maintainability and data protection, don’t completely hide important implementation details of your class.
- If you want to create read-only properties, don’t implement the
@attr.setter method. Users will be able to access the property but not modify it.
- Always remember that encapsulation is a convention, not an enforced aspect of Python syntax.
- For simple classes, consider using dataclasses which allow you to enable class encapsulation with a single line of code. However, dataclasses are for simpler classes with predictable attributes and methods. Check out this dataclasses tutorial to learn more.
Conclusion and Further Resources
In this tutorial, we’ve learned one of the core pillars of object-oriented programming in Python: encapsulation.
Encapsulation allows you to define controlled access to data stored inside objects of your class. This allows you to write clean, readable, and efficient code and prevent accidental changes or deletion of your class data.
Here are some more related resources to enhance your OOP knowledge:
Encapsulation is a fundamental object-oriented principle in Python. It protects your classes from accidental changes or deletions and promotes code reusability and maintainability. Consider this simple class definition:
Many Python programmers define classes like this. However, it is far from the best practices that pro Pythonistas follow. The problem with this class is evident when you try to modify its data:
Imagine an iPhone running on Android — what an outrage! Clearly, we need to set some boundaries within our class so that users can’t change its attributes to whatever they want.
If they do decide to change them, the changes must be under our terms, following our rules. But we still want the
.osor.brandattributes to stay the same on the surface.All of these things can be promoted using encapsulation and in this tutorial, we will learn all about it. Let’s get started!
How is Encapsulation Achieved in Python?
Python supports encapsulation through conventions and programming practices rather than enforced access modifiers. You see, the fundamental principle behind much of Python code design is “We are all adults here.” We can only implement encapsulation as a mere convention and expect other Python developers to trust and respect our code.
In other OOP languages such as Java and C++, encapsulation is strictly enforced with access modifiers such as
public,privateorprotected, but Python doesn't have those:_protected,__private)public,protected,privatekeywordspublic,protected,privatekeywords@propertydecorator for controlled accessSo, most, if not all, of the encapsulation techniques I am about to show you are Python conventions. They can easily be broken if you decide. But I trust that you respect and follow them in your own development projects.
Access modifiers in Python
Let’s say we have this simple class:
It has a single height attribute that we can print. The problem is that we can also change it to whatever we want:
So, how do we tell users that changing height is off-limits? Well, we could turn it into a protected member by adding a single preceding underscore:
Now, people who are aware of this convention will know that they can only access the attribute and that we are strongly discouraging them from using and modifying it. But if they want, they can modify it, oh yes.
So, how do we prevent that too? By using another convention — turn the attribute into a private member by adding double preceding underscores:
Now, Python will raise an error if someone tries to access the attribute, let alone modify it.
But do you notice what we just did? We hid the only information related to our objects from users. Our class just became useless because it has no public attributes.
So, how do we expose tree height to users but still control how they are accessed and modified? For example, we want tree heights to be within a specific range and only have integer values. How do we enforce that?
At this point, your Java-using friend might chime in and suggest using getter and setter methods. So, let’s try that first:
In this way, you create a private attribute
__heightbut let users access and modify it in a controlled way usingget_heightandset_heightmethods.Before setting a new value,
set_heightensures that the new height is within a certain range and numeric.But these methods seem like overkill for a simple operation. Besides, it is ugly to write code like this:
Wouldn’t it be more beautiful and readable if we could write this code:
and still enforce the correct data type and range for height? The answer is yes and we will learn how to do just that in the next section.
Using @property decorator in Python classes
We introduce a new technique — creating properties for attributes:
We want users to access a hidden attribute named
__heightas if it were a normal attribute calledheight. To achieve this, we define a method namedheightthat returnsself.__heightand decorate it with@property.Now, we can call
heightand access the private attribute:But the best part is that users can’t modify it:
So, we add another method called
height(self, new_height)that is wrapped by aheight.setterdecorator. Inside this method, we implement the logic that enforces the desired data type and range for height:Now, when a user tries to modify the
heightattribute,@height.setteris called, thus ensuring the correct value is passed:We can also customize how the
heightattribute is accessed through dot-notation with@height.getter:Even though we created
pinewith an integer height, we could modify its value with@height.getter.These were examples of how we could promote encapsulation in a Python class. Remember, encapsulation is still a convention because we can still break the internal
__heightprivate member:Everything in Python classes is public, and so are private methods. It isn’t a design flaw but an instance of the “We are all adults here” approach.
Here's an overview of the Python access modifiers we just reviewed:
self.attributeself._attributeself.__attribute@property decorator@property def attribute(self):@attribute.setter; allows read-only access to an attribute@property def attribute(self):Best Practices When Implementing Encapsulation
There are a number of best practices you can follow to ensure that your code aligns well with code written by other experienced OOPistas:
attr.getterandattr.settermethods can turn into a headache._).@attr.settermethod. Users will be able to access the property but not modify it.Conclusion and Further Resources
In this tutorial, we’ve learned one of the core pillars of object-oriented programming in Python: encapsulation.
Encapsulation allows you to define controlled access to data stored inside objects of your class. This allows you to write clean, readable, and efficient code and prevent accidental changes or deletion of your class data.
Here are some more related resources to enhance your OOP knowledge: