Skip to main content
HomeTutorialsPython

Encapsulation in Python Object-Oriented Programming: A Comprehensive Guide

Learn the fundamentals of implementing encapsulation in Python object-oriented programming.
Apr 2024  · 11 min read

Why Learn Encapsulation?

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

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.

So, most, if not all, 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)
20

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
50
pine.height = "Grandma"
pine.height
'Grandma'

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
20

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

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

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:

# Increase height by 5
pine.set_height(pine.get_height() + 5)

Wouldn’t it be more beautiful and readable if we could write this code:

pine.height += 5

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):
       # First, create a private or protected attribute
       self.__height = height

   @property
   def height(self):
       return self.__height

pine = Tree(17)
pine.height
17

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:

pine.height
17

But the best part is that users can’t modify it:

pine.height = 15
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  # Calling @height.setter
pine.height = 45  # An error is raised
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):
       # You can return a custom version of height
       return f"This tree is {self.__height} meters"


pine = Tree(33)

pine.height
'This tree is 33 meters'

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.

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:

  1. 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.
  2. 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.
  3. Consider raising a warning every time a user accesses a protected member (_).
  4. Use private members sparingly, as they can make code unreadable for those unfamiliar with the convention.
  5. Prioritize clarity over obscurity. As encapsulation aims to improve code maintainability and data protection, don’t completely hide important implementation details of your class.
  6. 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.
  7. Always remember that encapsulation is a convention, not an enforced aspect of Python syntax.
  8. 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:


Photo of Bex Tuychiev
Author
Bex Tuychiev

I am a data science content creator with over 2 years of experience and one of the largest followings on Medium. I like to write detailed articles on AI and ML with a bit of a sarcastıc style because you've got to do something to make them a bit less dull. I have produced over 130 articles and a DataCamp course to boot, with another one in the makıng. My content has been seen by over 5 million pairs of eyes, 20k of whom became followers on both Medium and LinkedIn. 

Topics

Continue Your Python Journey Today!

Course

Object-Oriented Programming in Python

4 hr
76.3K
Dive in and learn how to create classes and leverage inheritance and polymorphism to reuse and optimize code.
See DetailsRight Arrow
Start Course
See MoreRight Arrow
Related

cheat sheet

LaTeX Cheat Sheet

Learn everything you need to know about LaTeX in this convenient cheat sheet!
Richie Cotton's photo

Richie Cotton

tutorial

Writing Custom Context Managers in Python

Learn the advanced aspects of resource management in Python by mastering how to write custom context managers.
Bex Tuychiev's photo

Bex Tuychiev

tutorial

How to Convert a List to a String in Python

Learn how to convert a list to a string in Python in this quick tutorial.
Adel Nehme's photo

Adel Nehme

tutorial

A Comprehensive Tutorial on Optical Character Recognition (OCR) in Python With Pytesseract

Master the fundamentals of optical character recognition in OCR with PyTesseract and OpenCV.
Bex Tuychiev's photo

Bex Tuychiev

11 min

tutorial

Python KeyError Exceptions and How to Fix Them

Learn key techniques such as exception handling and error prevention to handle the KeyError exception in Python effectively.
Javier Canales Luna's photo

Javier Canales Luna

6 min

code-along

Full Stack Data Engineering with Python

In this session, you'll see a full data workflow using some LIGO gravitational wave data (no physics knowledge required). You'll see how to work with HDF5 files, clean and analyze time series data, and visualize the results.
Blenda Guedes's photo

Blenda Guedes

See MoreSee More