Public, Protected, and Private Members in Python

Learn how Python uses naming conventions for public, protected, and private class members, and how to apply underscore prefixes to control attribute visibility.

6 min read

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:

pythonpython
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] = value

The 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.

pythonpython
    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:

pythonpython
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.__value

The 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:

pythonpython
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What are public, protected, and private members in Python?

In Python, all class members are technically public and accessible from anywhere. The language uses naming conventions rather than keywords to indicate intended visibility. A name with no leading underscore is public and part of the class's API. A name with a single leading underscore is protected by convention, meaning it is an internal detail that external code should not access. A name with a double leading underscore triggers name mangling, making it harder to access accidentally, and is used to avoid naming collisions in subclasses.

Does Python enforce protected or private access?

No, Python does not enforce access restrictions at runtime. The single underscore is purely a convention, and the double underscore name mangling only changes the attribute name internally. External code can still access mangled names if it knows the mangled form. Python's philosophy is to trust developers to follow conventions rather than forcing restrictions through the language.

When should I use a double underscore prefix?

Use double underscores sparingly, primarily when you are writing a class that will be subclassed and you need to prevent the subclass from accidentally overriding an internal attribute that your methods depend on. Name mangling ensures that the attribute name includes the class name, so a subclass attribute with the same name will not collide. For most application code, a single underscore is sufficient to communicate that an attribute is internal.

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.