Python
private variables
class encapsulation
object-oriented programming
data hiding

Does Python have "private" variables in classes?

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Python does not have true private variables like Java, C++, or C#. Instead, it uses naming conventions and a mechanism called name mangling to indicate that certain attributes should not be accessed directly. Python's philosophy is "we are all consenting adults" — it trusts developers to follow conventions rather than enforcing access restrictions.

The Three Levels of Access

Public: name

No underscore prefix. Intended for external use:

python
1class User:
2    def __init__(self, name, email):
3        self.name = name      # Public
4        self.email = email    # Public
5
6user = User("Alice", "[email protected]")
7print(user.name)   # "Alice" — fully accessible

Protected (Convention): _name

Single underscore prefix. Signals "for internal use" but is not enforced:

python
1class User:
2    def __init__(self, name, email):
3        self.name = name
4        self._email = email   # Convention: internal use
5
6user = User("Alice", "[email protected]")
7print(user._email)  # Still works — just a convention

The single underscore is a widely respected convention. Linters and IDEs may warn when accessing _ prefixed attributes from outside the class, but Python does not prevent it.

"Private" (Name Mangling): __name

Double underscore prefix triggers name mangling — Python renames the attribute to _ClassName__name:

python
1class User:
2    def __init__(self, name, email):
3        self.name = name
4        self.__email = email  # Name-mangled
5
6user = User("Alice", "[email protected]")
7# print(user.__email)        # AttributeError!
8print(user._User__email)     # "[email protected]" — still accessible

Name mangling makes accidental access harder but does not truly prevent it. Its primary purpose is to avoid name collisions in inheritance hierarchies, not to enforce privacy.

Name Mangling in Detail

Python transforms __attribute to _ClassName__attribute at compile time:

python
1class Parent:
2    def __init__(self):
3        self.__value = 10    # Stored as self._Parent__value
4
5class Child(Parent):
6    def __init__(self):
7        super().__init__()
8        self.__value = 20    # Stored as self._Child__value
9
10child = Child()
11print(child._Parent__value)   # 10 — Parent's "private" value
12print(child._Child__value)    # 20 — Child's "private" value

Without name mangling, both self.__value assignments would overwrite each other. Name mangling keeps them separate.

Using @property for Controlled Access

The Pythonic way to control attribute access is the @property decorator:

python
1class BankAccount:
2    def __init__(self, initial_balance):
3        self._balance = initial_balance  # Convention: internal
4
5    @property
6    def balance(self):
7        """Read-only access to balance."""
8        return self._balance
9
10    def deposit(self, amount):
11        if amount <= 0:
12            raise ValueError("Deposit must be positive")
13        self._balance += amount
14
15    def withdraw(self, amount):
16        if amount > self._balance:
17            raise ValueError("Insufficient funds")
18        self._balance -= amount
19
20account = BankAccount(100)
21print(account.balance)       # 100 — getter works
22# account.balance = 500      # AttributeError — no setter defined
23account.deposit(50)
24print(account.balance)       # 150

With Getter and Setter

python
1class Temperature:
2    def __init__(self, celsius):
3        self._celsius = celsius
4
5    @property
6    def celsius(self):
7        return self._celsius
8
9    @celsius.setter
10    def celsius(self, value):
11        if value < -273.15:
12            raise ValueError("Temperature below absolute zero")
13        self._celsius = value
14
15    @property
16    def fahrenheit(self):
17        return self._celsius * 9/5 + 32
18
19temp = Temperature(25)
20print(temp.celsius)      # 25
21print(temp.fahrenheit)   # 77.0
22temp.celsius = 30        # Setter validates the value
23# temp.celsius = -300    # ValueError!

Comparison with Other Languages

LanguageMechanismEnforced?
Python_ convention, __ name manglingNo (convention-based)
Javaprivate keywordYes (compile-time, but reflection bypasses)
C++private: sectionYes (compile-time)
C#private keywordYes (compile-time, but reflection bypasses)
JavaScript# prefix (ES2022)Yes (runtime enforcement)
Rubyprivate methodYes (but send() bypasses)

Even in languages with "true" private access, reflection or other mechanisms can often bypass it. Python simply makes this explicit rather than pretending privacy is absolute.

__slots__ for Attribute Restriction

If you want to prevent adding arbitrary attributes to a class, use __slots__:

python
1class Point:
2    __slots__ = ('x', 'y')
3
4    def __init__(self, x, y):
5        self.x = x
6        self.y = y
7
8p = Point(1, 2)
9# p.z = 3  # AttributeError: 'Point' object has no attribute 'z'

__slots__ restricts which attributes can exist, but does not control read/write access.

Common Pitfalls

  • Trust the Convention: When working in a team, respect the underscore conventions as a contract regarding intended use. Accessing _internal attributes from outside the class makes your code fragile — internal APIs can change without warning.
  • Use Properties: Instead of relying purely on name mangling, use @property to manage attribute access. Properties provide validation, computed values, and clear interfaces.
  • Dunder confusion: Attributes with double underscores on both sides (__init__, __str__) are Python's special methods (dunder methods), not private attributes. Do not use double leading and trailing underscores for your own attributes.
  • Serialization: Name-mangled attributes can cause issues with pickle, json, and other serialization libraries that inspect attribute names. Prefer single-underscore convention for attributes that need to be serialized.
  • Testing: Name mangling makes unit testing harder because tests outside the class must use _ClassName__attr. This is another reason to prefer _single_underscore and @property.

Summary

  • Python has no true private keyword — access control is by convention
  • _name: "protected" convention — internal use, not enforced
  • __name: name mangling to _ClassName__name — avoids name collisions in inheritance
  • Use @property for controlled, validated access to attributes
  • Follow the conventions — they are the social contract of Python development

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.