Python public, protected, and private members are a topic that frequently confuses developers coming from languages like Java, C++, or C#, where the compiler enforces strict access control through dedicated keywords. In those languages, marking a field as private means the compiler will refuse to compile any code that tries to access that field from outside the class. Python takes a fundamentally different approach. It has no private keyword, no protected keyword, and no compiler-enforced access restrictions of any kind. Instead, Python uses naming conventions to communicate intent, and it trusts developers to respect those conventions. This article explains exactly how the underscore conventions work, what name mangling does and does not do, and how to apply these conventions effectively in your own classes.
The Python philosophy on access control is often summarized by the phrase "we are all consenting adults here." The language designers made a deliberate choice not to build access restrictions into the runtime because they believed that developers should have the freedom to access internals when they have a legitimate reason, such as debugging, testing, or working around a library bug. The naming conventions exist to communicate which parts of a class are stable public API and which are implementation details that might change without notice. If you access an underscored attribute from outside the class, the language will not stop you, but you accept the risk that your code may break in a future update.
If you have read the article on encapsulation in Python, you already understand the broader motivation for hiding implementation details. This article focuses specifically on the mechanics of the naming conventions: what each level of underscore prefix means, how the interpreter handles them, and how to choose the right level for each attribute and method in your class.
Public members: no underscore prefix
In Python, a name with no leading underscore is public by default. This is the simplest and most common case. Attributes and methods that are part of the class's intended interface, the ones you expect external code to use, should have no underscore prefix. The name communicates that this member is stable, documented, and safe to depend on. If you change or remove a public attribute in a future version of your class, you should consider that a breaking change that requires updating all code that uses the class.
Most attributes that are set in the constructor and are meant to be read directly by external code are public. A Customer class might have public name and email attributes. A Point class might have public x and y attributes. A Configuration class might have public attributes for each setting that callers are expected to read and possibly modify. Public attributes are the simplest case and require no special syntax or convention beyond choosing a good name.
The key design decision for public attributes is whether to make them plain attributes or properties. A plain attribute stores a value and lets external code read and write it freely. A property looks the same to external code but runs methods behind the scenes, allowing validation, computation, and read-only behavior. The article on the property decorator later in this section covers how to make that choice and how to migrate from a plain attribute to a property without breaking existing code.
Protected members: the single leading underscore
A name with a single leading underscore, such as _internal_data or _helper_method, is the standard Python convention for a member that is an implementation detail and not part of the class's public API. External code should not access these members directly. They may change or disappear in future versions of the class without warning. The underscore serves as documentation for anyone reading the code, and every Python developer understands its meaning.
The single underscore convention applies broadly. Instance attributes that store internal state, helper methods that exist only to be called by other methods in the same class, and module-level functions that are not part of the module's public API all use the single underscore prefix. The convention is not limited to classes; it is a universal Python idiom. When you see a name starting with an underscore in any Python code, you know that the author intends it to be private to its containing scope.
Here is a class that uses the single underscore convention to separate its public interface from its internal implementation:
class Cache:
def __init__(self, max_size):
self._max = max_size
self._data = {}
self._access_count = 0
def get(self, key):
self._access_count += 1
return self._data.get(key)
def set(self, key, value):
if len(self._data) >= self._max:
self._evict_oldest()
self._data[key] = valueThe public interface consists of get and set. The internal state, including the maximum size, the data dictionary, and the access counter, is all marked with underscores.
def _evict_oldest(self):
oldest = next(iter(self._data))
del self._data[oldest]The eviction logic is in a method prefixed with an underscore because external code has no reason to call it directly. If a future version of the cache changes its eviction strategy from removing the oldest entry to removing the least recently used entry, the public get and set methods might not change at all, but the internal _evict_oldest method might be replaced entirely. Code that called _evict_oldest directly would break; code that only used get and set would continue to work.
Private members: the double leading underscore and name mangling
A name with a double leading underscore and at most one trailing underscore, such as __private_data or __internal_method, triggers Python's name mangling behavior. The interpreter rewrites the name to include the class name as a prefix, making it harder to access the attribute accidentally from outside the class or from a subclass. A method named __validate in a class called Form becomes _Form__validate in the object's attribute dictionary. The purpose of name mangling is not to make the attribute inaccessible; it is to prevent accidental name collisions when a subclass defines an attribute with the same name as an internal attribute in the parent class.
The following example demonstrates the collision-avoidance purpose of name mangling with a parent class and a child class that both use a double-underscore attribute:
class Parent:
def __init__(self):
self.__value = 10
def get_parent_value(self):
return self.__value
class Child(Parent):
def __init__(self):
super().__init__()
self.__value = 20
def get_child_value(self):
return self.__valueThe Parent stores its value under _Parent__value, and the Child stores its separate value under _Child__value. The two attributes coexist without collision. Now create an instance and verify:
obj = Child()
print(obj.get_parent_value())
print(obj.get_child_value())The get_parent_value method reads the parent's mangled attribute, and get_child_value reads the child's. The output is 10 and 20, confirming that the two values are stored independently despite having the same name in the source code. Without name mangling, the child's assignment would have overwritten the parent's attribute, and both methods would return 20.
Name mangling is a specialized tool for a specific problem, and it should not be used as a general-purpose privacy mechanism. It makes debugging harder because attribute names in error messages and stack traces appear in their mangled form. It makes testing more cumbersome because test code that needs to inspect internal state must use the mangled name. It can also cause unexpected behavior in multiple inheritance hierarchies where the name mangling rules interact with the method resolution order. For most classes, a single underscore is the right choice. Reserve double underscores for situations where you are writing a class that will be subclassed by unknown third-party code and you need to protect internal attributes that are critical to the class's correct functioning.
The article on class methods in Python covers how decorators distinguish between different method types, and the underscore conventions covered here serve a parallel purpose for attributes. Both are mechanisms for communicating intent rather than enforcing restrictions, and both rely on the shared understanding of the Python community to function effectively.
Rune AI
Key Insights
- Python has no public, protected, or private keywords; all members are technically accessible from anywhere.
- A single leading underscore is the universal convention for internal members that external code should not depend on.
- A double leading underscore triggers name mangling that prepends the class name to avoid accidental collisions in subclasses.
- Name mangling is a collision-avoidance tool, not a security mechanism; mangled names can still be accessed if you know the mangled form.
- Use single underscores for most internal members; reserve double underscores for situations where subclass name collisions are a real risk.
Frequently Asked Questions
What are public, protected, and private members in Python?
Does Python enforce protected or private access?
When should I use a double underscore prefix?
Conclusion
Python's approach to member visibility replaces compiler-enforced access control with clear naming conventions. The single underscore says 'this is internal, do not depend on it.' The double underscore says 'this is internal and I need to prevent accidental name collisions in subclasses.' Both conventions rely on developer trust and communication rather than enforcement, which is consistent with Python's design philosophy of explicitness and consent.
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.