LearnContact
Lesson 3725 min read

Encapsulation

Learn how encapsulation bundles data and behavior together and how Python uses naming conventions and properties to protect internal state.

Introduction

Imagine a bank account object that lets any piece of code directly set its balance to a negative number, or a car object that lets you set its speed to -50. Without some way to protect internal data, objects can end up in states that make no sense. Encapsulation is the OOP principle that solves this problem.

In this lesson you will learn what encapsulation means, how Python approaches "privacy" differently from languages like Java or C++, and how to control access to an object's internal state using naming conventions and the @property decorator.

What You Will Learn
  • What encapsulation means and why it matters.
  • Python's underscore-based privacy conventions (_name and __name).
  • How name mangling works with double underscores.
  • How to write getter and setter methods.
  • How to use @property for clean, controlled attribute access.

What is Encapsulation?

Encapsulation means bundling data (attributes) and the methods that operate on that data into a single unit — a class — while restricting direct, uncontrolled access to some of that data from outside the class. Instead of letting outside code poke at internal values freely, the class exposes a controlled interface for reading or changing them.

You have already been using encapsulation informally since Lesson 35 — every class bundles attributes and methods together. This lesson focuses specifically on the "restricting access" half of that definition.

Bundling

Data and the methods that work on that data live together inside one class.

Restricting Access

Internal details are hidden or protected from direct outside interference.

Controlled Interface

Outside code interacts through methods, not by reaching directly into attributes.

Data Integrity

Invalid values can be rejected before they ever get stored on the object.

No True "Private" Keyword

Unlike Java or C++, Python has no private keyword that truly blocks access. Instead, Python relies on naming conventions — an agreement among developers about what should and should not be touched directly. This philosophy is often summarized as "we are all consenting adults here."

Public Attributes (The Default)

By default, every attribute in Python is public, meaning any code outside the class can read or modify it directly.

public.py
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

acc = Account("Riya", 1000)
print(acc.balance)

acc.balance = -5000   # Nothing stops this!
print(acc.balance)
Output
1000
-5000

Nothing prevented us from setting an invalid, negative balance. This is exactly the problem encapsulation is designed to prevent.

Protected Attributes: Single Underscore

Prefixing an attribute name with a single underscore (for example _balance) is a convention that signals "this is intended for internal use — treat it as protected, not part of the public interface." Python does not actually enforce this; it is purely a signal to other developers (and to tools like IDEs and linters).

protected.py
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance   # protected by convention

acc = Account("Riya", 1000)
print(acc._balance)   # still accessible, but discouraged
Output
1000
Convention, Not Enforcement

A single leading underscore is a polite "do not touch" sign, not a lock. Well-behaved code respects it and accesses the value through methods instead.

Private Attributes: Double Underscore

Prefixing an attribute with two leading underscores (for example __balance) triggers Python's name mangling, which makes the attribute much harder — though still not impossible — to access accidentally from outside the class.

private.py
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance   # name-mangled

    def get_balance(self):
        return self.__balance

acc = Account("Riya", 1000)
print(acc.get_balance())

try:
    print(acc.__balance)
except AttributeError as e:
    print("Error:", e)
Output
1000
Error: 'Account' object has no attribute '__balance'

Name Mangling

When Python sees an attribute named __balance inside a class named Account, it internally renames it to _Account__balance. This is called name mangling, and it is why accessing acc.__balance from outside fails, while the attribute still technically exists (and is reachable) under its mangled name.

name_mangling.py
acc = Account("Riya", 1000)
print(acc._Account__balance)   # works, but you should never write this
Output
1000
Not True Security

Name mangling prevents accidental access and naming collisions in subclasses — it is not a security feature. Determined code can still find the mangled name. Treat double underscores as "strongly discouraged to touch," not "impossible to touch."

Getters and Setters

A common pattern for controlling access to a private attribute is to write explicit getter and setter methods: a method to retrieve the value, and a method to change it, with the setter validating the new value before accepting it.

getters_setters.py
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance

    def get_balance(self):
        return self.__balance

    def set_balance(self, amount):
        if amount < 0:
            print("Balance cannot be negative.")
        else:
            self.__balance = amount

acc = Account("Riya", 1000)
acc.set_balance(-500)
print(acc.get_balance())

acc.set_balance(2500)
print(acc.get_balance())
Output
Balance cannot be negative.
1000
2500

The setter rejects the invalid value, so the object never enters an invalid state. This is the core benefit of encapsulation in action.

The @property Decorator

Calling get_balance() and set_balance() everywhere works, but it is a bit clunky compared to plain attribute access. Python offers the @property decorator, which lets a method be accessed using attribute syntax (no parentheses) while still running your validation logic behind the scenes.

property_decorator.py
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance

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

    @balance.setter
    def balance(self, amount):
        """Setter"""
        if amount < 0:
            raise ValueError("Balance cannot be negative.")
        self.__balance = amount

acc = Account("Riya", 1000)
print(acc.balance)      # reads like an attribute, calls the getter

acc.balance = 3000      # looks like assignment, calls the setter
print(acc.balance)

try:
    acc.balance = -100
except ValueError as e:
    print("Error:", e)
Output
1000
3000
Error: Balance cannot be negative.
Property in Short

@property turns a method into a read-like attribute, and the matching @name.setter turns another method into a write-like attribute. The caller writes simple attribute syntax; your class still enforces its rules underneath. A deeper dive into properties and read-only attributes comes in a later lesson.

Why Encapsulation Matters

Encapsulation is not just about hiding things for the sake of it — it directly improves how maintainable and reliable your code is.

Prevents Invalid State

Setters can reject values that would break the object's assumptions.

Easier to Change Internals

You can change how data is stored internally without breaking code that uses the class, as long as the public interface stays the same.

Clear Public Interface

Users of your class know exactly which methods and attributes are meant to be used.

Common Mistakes

Avoid These Mistakes
  • Thinking double underscores make an attribute truly unbreakable — it is name mangling, not real security.
  • Overusing double underscores everywhere out of habit; a single underscore is often enough.
  • Writing getters and setters for every single attribute even when no validation is needed — plain public attributes are fine for simple data.
  • Forgetting the @name.setter decorator must use the exact same method name as the @property getter.

Best Practices

  • Use a single underscore (_name) for "internal use" attributes by default.
  • Reserve double underscores (__name) for cases where name collisions in subclasses are a real concern.
  • Prefer @property over manual get_x()/set_x() methods for a cleaner interface in modern Python code.
  • Validate input inside setters rather than trusting that callers will always pass sane values.
  • Do not add getters/setters for attributes that never need validation — that just adds noise.

Frequently Asked Questions

Does Python have truly private attributes like Java?

No. Python relies on naming conventions (_name and __name) rather than a hard "private" keyword. Determined code can still access anything if it really tries.

What is the difference between _name and __name?

_name is just a convention meaning "internal use, please do not touch" — Python does nothing special with it. __name additionally triggers name mangling, renaming the attribute internally to _ClassName__name.

Should I use @property for every attribute?

No. Use plain public attributes for simple data with no rules attached, and reach for @property only when you need validation, computed values, or controlled read/write access.

Is encapsulation only about privacy?

Mostly it is about controlling access and protecting valid state, but it also includes the more general idea of bundling related data and behavior together inside a class.

Does encapsulation slow down my program?

The overhead of a getter, setter, or property call is negligible for virtually all real-world programs. The benefit to code correctness and maintainability far outweighs the tiny performance cost.

Key Takeaways

  • Encapsulation bundles data and methods together and restricts uncontrolled access to internal state.
  • Python has no true "private" keyword — it uses underscore-based naming conventions instead.
  • A single underscore (_name) means "protected/internal use only" by convention.
  • A double underscore (__name) triggers name mangling to _ClassName__name.
  • Getters, setters, and the @property decorator let you control and validate access to attributes.

Summary

Encapsulation protects an object's internal state by controlling how it can be read or changed. Python achieves this not through strict access modifiers, but through naming conventions — single underscores for protected attributes, double underscores for name-mangled ones — combined with getter/setter methods or the cleaner @property decorator.

With encapsulation covered, you are ready to explore inheritance — how classes can build on top of one another to reuse and extend behavior.

Lesson 37 Completed
  • You understand what encapsulation means and why it matters.
  • You can use _name and __name conventions correctly.
  • You understand how name mangling works.
  • You can write getters, setters, and @property-based access.
  • You are ready to learn about inheritance.
Next Lesson →

Inheritance