Skip to content

Encapsulation in Python: Hiding Implementation Details and Exposing a Public Interface

Updated: at 03:34 AM

Encapsulation is one of the fundamental concepts in object-oriented programming (OOP) that refers to bundling data and methods within a single unit or object. This allows sensitive data to be hidden from other parts of the program while only exposing a public set of functions or methods to interact with the object.

Encapsulation provides several key advantages:

Python supports encapsulation through mechanisms like private attributes, getter/setter methods, and properties. This guide will demonstrate how to implement encapsulation in Python by:

We’ll also cover when to use encapsulation and look at examples illustrating its advantages. Follow along to learn how encapsulation works in Python!

Table of Contents

Open Table of Contents

Denoting Private Attributes with Underscores

In Python, there are no truly private methods or attributes like in some other OOP languages. However, by convention, any attribute prefixed with a double underscore __ is considered private within a class.

For example:

class Person:

  def __init__(self, name):
    self.__name = name

Here __name is treated as a private attribute inside Person. To access or modify it, methods within Person can refer to self.__name directly. But outside the class, it should not be accessed directly.

This naming convention alerts other developers not to mess with those attributes, since modifying them could break internal logic inside the class. But Python does not actually prevent outside code from accessing those names. We’ll explore ways to emulate true privateness later.

Using Getters and Setters

Instead of accessing __name directly on a Person instance, we can define public methods to get and set its value in a controlled way:

class Person:

  def __init__(self, name):
    self.__name = name

  def get_name(self):
    return self.__name

  def set_name(self, new_name):
    self.__name = new_name

p = Person('Adam')

print(p.get_name()) # Adam
p.set_name('John')
print(p.get_name()) # John

This allows the class to have full control over how its attributes can be accessed and modified by external code. For example, set_name() could validate data before updating __name, ensuring it remains in a correct state.

Getters and setters allow encapsulation of behavior along with data. Other benefits like lazy evaluation and caching can also be added.

Using the @property Decorator

Manually writing getter and setter methods can become tedious. Python provides a built-in @property decorator to create them automatically:

class Person:

  def __init__(self, name):
    self.__name = name

  @property
  def name(self):
    return self.__name

  @name.setter
  def name(self, new_name):
    self.__name = new_name

p = Person('Adam')

print(p.name) # Adam
p.name = 'John'
print(p.name) # John

The @property decorator converts the name() method into a getter function. A setter function is created using @name.setter.

This allows p.name to be accessed like a normal attribute while letting the class manage access behind the scenes.

Understanding Name Mangling

As mentioned earlier, Python doesn’t truly prevent accessing attributes prefixed with double underscores. But it does employ a technique called name mangling to make it more difficult.

When a class attribute is prefixed with __, Python changes its name internally by adding _<ClassName> in front of it.

For example:

class Person:

  def __init__(self, name):
    self.__name = name

p = Person('Adam')
print(p.__name) # AttributeError - cannot access directly

print(p._Person__name) # 'Adam' - name mangled version accessible

So __name is mangled to _Person__name, which can still be accessed externally if you know the name.

Name mangling is not impermeable security, but it reduces chances of accidentally accessing those attributes. It also prevents subclasses from accidentally overriding private attributes.

When to Use Encapsulation

Encapsulation is most useful when:

For simple data classes, encapsulation may be overkill. But for non-trivial objects encapsulation makes code more robust, reusable, and maintainable.

Encapsulation Example

Here is a real-world example to illustrate the benefits of encapsulation:

class BankAccount:

  OVERDRAFT_LIMIT = -500

  def __init__(self, balance=0):
    self.__balance = balance

  def deposit(self, amount):
    self.__balance += amount

  def withdraw(self, amount):
    if self.__balance - amount >= BankAccount.OVERDRAFT_LIMIT:
      self.__balance -= amount
    else:
      print("Cannot withdraw beyond overdraft limit.")

  @property
  def balance(self):
    return self.__balance

# Test the BankAccount
my_account = BankAccount(500)

# Direct access not allowed
# my_account.__balance = 1000000

my_account.withdraw(600)
print(my_account.balance) # 500

my_account.deposit(200)
print(my_account.balance) # 700

my_account.withdraw(800)
print(my_account.balance) # 700 (overdraft prevented)

This class encapsulates the __balance attribute making it inaccessible from outside. The withdrawal logic also prevents overdrafting limited to -500.

The balance is exposed using a property for external read access but not write access preventing anyone from arbitrarily setting it.

This encapsulation ensures correct behavior and state for BankAccount objects.

Conclusion

Encapsulation is an essential principle in object-oriented design and Python’s implementation provides a straightforward way to achieve it. Key takeaways include:

Applying encapsulation appropriately improves code organization, decreases coupling, and increases security. It takes Python’s support for OOP to the next level.

By mastering these techniques, you can write Python code that is more modular, extensible, and less prone to bugs.