Encapsulation in Python is the practice of hiding the internal details of a class so that code outside the class does not depend on how the class is implemented. When you write a class, you make a distinction between its public interface, the methods and attributes that external code is meant to use, and its internal implementation, the attributes and helper methods that exist only to support the public interface. This distinction is the foundation of maintainable object-oriented code because it lets you change how a class works internally without breaking every piece of code that uses the class. If external code accesses your internal attributes directly, any change to those attributes forces you to find and update every access point. If external code only calls your public methods, you can reorganize the internals freely as long as the public methods keep their same signatures and behavior.
Python's approach to encapsulation is different from languages like Java and C++, and understanding that difference prevents a common mistake among programmers coming to Python from those languages. Java has keywords like private and protected that the compiler enforces, preventing external code from accessing marked members at all. Python has no such keywords and no compiler-enforced access restrictions. Instead, Python relies on naming conventions and a cultural norm best summarized by the phrase "we are all consenting adults." The language trusts developers to respect the convention that attributes prefixed with a single underscore are internal and should not be accessed from outside the class. This trust-based approach is a deliberate design choice, not a missing feature, and it reflects Python's philosophy that developers should be free to access internals when they have a good reason to do so.
If you have worked through the articles on instance attributes and instance methods, you already know how to define the data and behavior that make up a class. Encapsulation adds the layer of deciding which of those attributes and methods are part of the class's public contract and which are internal details that may change in future versions. This article explains the conventions Python provides for making that distinction clear and the tools, particularly the property decorator, for controlling attribute access when simple naming conventions are not enough.
The single underscore convention
In Python, a name prefixed with a single underscore is the universal signal that the name is an internal implementation detail. This applies to attributes, methods, functions, and even modules. When you see self._balance in a class, you know that the balance attribute is not part of the class's public API and that external code should use whatever public methods the class provides instead of accessing the balance directly. The underscore is not enforced by the interpreter; external code can still write account._balance and read or modify the value. But doing so is understood to be a violation of the class's intended interface, and the developer who does it accepts the risk that the attribute may change or disappear in a future version of the class.
The single underscore convention is sufficient for most encapsulation needs in Python. It tells readers of your code which attributes they should depend on and which they should treat as off-limits. It keeps your class's public interface small and focused, which makes the class easier to document, easier to test, and easier to change. The following example shows a class where the public interface consists of two methods and the internal state is marked with underscores:
class Thermostat:
def __init__(self, target_temp):
self._target = target_temp
self._current = target_temp
def set_target(self, temperature):
if 10 <= temperature <= 30:
self._target = temperature
def current_temperature(self):
return self._currentThe underscore tells anyone reading this code that _target and _current are internal details. External code should use set_target to change the target and current_temperature to read the current value. If a future version of the class stores the temperature in Fahrenheit internally and converts on the way in and out, code that accessed _target directly would break, but code that used the public methods would continue to work because the methods can be updated to perform the conversion.
The double underscore and name mangling
Python provides a second level of naming convention with the double underscore prefix. A name that begins with two underscores and does not end with two underscores triggers a behavior called name mangling. The Python interpreter rewrites the name internally to include the class name, making it harder to access accidentally from outside the class or from subclasses. A method named __validate inside a class called Form becomes _Form__validate in the object's internal dictionary. This is not a security mechanism and does not make the attribute truly private; it is a collision-avoidance tool designed to prevent subclasses from accidentally overriding attributes that the parent class relies on.
The following example demonstrates name mangling in practice and shows that the attribute is still accessible if you know the mangled name:
class Parent:
def __init__(self):
self.__secret = "hidden"
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = "visible"
obj = Child()
print(obj._Parent__secret)
print(obj._Child__secret)The Parent class stores its __secret under the mangled name _Parent__secret, and the Child class stores its own __secret under _Child__secret. The two attributes coexist without collision, which is exactly what name mangling is designed to achieve. The output shows "hidden" for the parent's attribute and "visible" for the child's. This pattern is useful in large frameworks and libraries where subclass authors might accidentally choose the same attribute name as an internal attribute in the parent class. For everyday application code, the single underscore convention is almost always sufficient, and double underscores should be used sparingly because they make debugging and testing more difficult.
Properties: controlled attribute access the Pythonic way
When a simple naming convention is not enough and you need to run code every time an attribute is read or written, Python provides the property decorator. A property looks like a regular attribute to external code, but behind the scenes it calls getter and setter methods that you define. Properties let you add validation, computed values, lazy loading, and read-only behavior without changing the public interface of the class. Code that accesses object.attribute does not need to know or care whether attribute is a plain variable or a property backed by methods.
The simplest property is a read-only computed attribute. Instead of storing a value and returning it, the getter computes the value on demand from other attributes. Here is a class that stores a first name and a last name and provides a full name as a property:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def full_name(self):
return f"{self.first} {self.last}"External code reads person.full_name as if it were a regular attribute, and the property method runs each time to compute the result from the current values of first and last. If either name changes, full_name automatically reflects the change because it is computed on every access rather than stored and potentially becoming stale.
For attributes that need validation on assignment, a property setter runs code every time a value is assigned. The following example adds a setter to ensure that a temperature value stays within a valid range:
class Sensor:
def __init__(self, reading):
self._reading = None
self.reading = reading
@property
def reading(self):
return self._reading
@reading.setter
def reading(self, value):
if not isinstance(value, (int, float)):
raise TypeError("Reading must be a number")
self._reading = valueThe internal value is stored in the underscored attribute _reading. External code accesses the reading property, which delegates to the getter and setter methods. The setter validates the input type before storing it, preventing invalid data from ever reaching the internal attribute. The caller writes sensor.reading = 42.5 and does not need to know that a method is running behind the scenes. This pattern is the Pythonic way to achieve controlled attribute access, and the article on the property decorator later in this section covers more advanced property patterns including deleters, cached properties, and properties that wrap complex business logic.
Rune AI
Key Insights
- Encapsulation means hiding internal implementation details so that external code depends only on a stable public interface.
- Python uses naming conventions rather than keywords to communicate visibility: a single underscore means internal, a double underscore triggers name mangling.
- The @property decorator creates managed attributes that look like regular attributes but run methods for access and assignment.
- Python trusts developers to respect conventions; do not try to force access control that the language does not enforce.
- Good encapsulation makes classes easier to change internally without breaking code that uses them.
Frequently Asked Questions
Does Python have true private attributes like Java or C++?
When should I use a single underscore prefix versus a double underscore prefix?
How do I provide controlled access to internal attributes in Python?
Conclusion
Encapsulation in Python is achieved through convention and trust, not through compiler-enforced access restrictions. The single leading underscore is the universal signal that an attribute is internal and should not be relied upon by external code. Properties provide a clean mechanism for controlled access when you need validation, computed values, or read-only attributes. Understanding Python's philosophy of 'we are all consenting adults' helps you write classes that are both safe and flexible.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.