Skip to main content

Encapsulation in Python: A Comprehensive Guide

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

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:

Feature Python Java C++
Access modifiers No enforced modifiers; uses naming conventions (_protected__private) Enforced with publicprotectedprivate keywords Enforced with publicprotectedprivate 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)
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.

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:

  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:

Learn Python From Scratch

Master Python for data science and gain in-demand skills.
Start Learning for Free

FAQs

How does encapsulation differ from abstraction in Python?

Encapsulation is about bundling data and methods that operate on the data within one unit, such as a class, and restricting access to some components. Abstraction, on the other hand, is about hiding the complex reality while exposing only the essential parts. While encapsulation focuses on restricting access, abstraction focuses on simplifying interactions.

Can encapsulation be used in functional programming, or is it exclusive to OOP?

Encapsulation is a concept tied closely to object-oriented programming (OOP) and is not typically used in functional programming. Functional programming emphasizes immutability and stateless functions, unlike OOP, which focuses on objects and encapsulating data within those objects.

What are some common pitfalls to avoid when implementing encapsulation in Python?

Common pitfalls include overusing private variables, which can make the code difficult to maintain, and not providing adequate access methods (getters/setters) which can lead to fragile code. Additionally, developers might forget that Python's encapsulation is by convention, and thus, it can be bypassed.

How can encapsulation improve code security?

Encapsulation can improve code security by restricting access to the internal state of an object and preventing unauthorized modifications. By controlling how data is accessed and modified, developers can ensure that the data remains valid and consistent.

How does the use of the @property decorator affect performance in Python?

Using the @property decorator can introduce slight overhead due to the extra method calls, but in most cases, this impact is negligible. The benefits of improved code readability and maintainability often outweigh any minor performance costs.

Is it possible to enforce encapsulation strictly in Python as in Java or C++?

Python does not enforce encapsulation strictly like Java or C++. Its philosophy is based on conventions and the understanding that developers will respect the intended use of private and protected members. However, using naming conventions like underscores can discourage misuse.

What are some real-world applications of encapsulation in Python projects?

Encapsulation is used to protect sensitive data in applications such as banking software, where account details are hidden. It's also used in libraries and frameworks to hide complex implementation details, allowing developers to interact with simple interfaces.

How can I test encapsulated code effectively in Python?

Testing encapsulated code involves writing unit tests for the public interface of a class. By testing the methods and properties exposed to the user, you ensure that the internal state changes are correctly managed and that the encapsulation logic holds.

Can encapsulation be applied to Python modules, or is it limited to classes?

While encapsulation is primarily associated with classes, the concept can be extended to modules by using underscore prefixed functions or variables to signal private usage within a module.

How does encapsulation relate to polymorphism in Python?

Encapsulation and polymorphism are both principles of OOP but serve different purposes. Encapsulation restricts access to certain parts of an object, while polymorphism allows objects to be treated as instances of their parent class, making it easier to expand and maintain code. They can be used together to create flexible and secure code structures.


Photo of Bex Tuychiev
Author
Bex Tuychiev
LinkedIn

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
85.6K
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

tutorial

Object-Oriented Programming in Python (OOP): Tutorial

Tackle the basics of Object-Oriented Programming (OOP) in Python: explore classes, objects, instance methods, attributes and much more!
Théo Vanderheyden's photo

Théo Vanderheyden

12 min

tutorial

Inner Classes in Python

In this basic Python tutorial, you'll learn about why and when you should use inner classes.
Hafeezul Kareem Shaik's photo

Hafeezul Kareem Shaik

5 min

tutorial

Python Data Classes: A Comprehensive Tutorial

A beginner-friendly tutorial on Python data classes and how to use them in practice
Bex Tuychiev's photo

Bex Tuychiev

9 min

tutorial

Logging in Python Tutorial

Learn about the fundamentals of Logging in Python.
Aditya Sharma's photo

Aditya Sharma

9 min

tutorial

Introduction to Python Metaclasses

In this tutorial, learn what metaclasses are, how to implement them in Python, and how to create custom ones.
Derrick Mwiti's photo

Derrick Mwiti

6 min

tutorial

Python Backend Development: A Complete Guide for Beginners

This complete guide teaches you the fundamentals of Python backend development. Learn basic concepts, frameworks, and best practices to start building web applications.
Oluseye Jeremiah's photo

Oluseye Jeremiah

26 min

See MoreSee More