Python Access Modifiers: How Encapsulation Really Works
python access modifiers: Learn how Python implements access control through underscore conventions, name mangling, and properties—and when each approach is appropriate.
Python access modifiers are a common source of confusion for developers coming from Java, C++, or C#. Those languages enforce private, protected, and public at compile time. Python does not. There is no private keyword, no protected keyword, and no compiler that rejects an out-of-class access. Instead, Python uses naming conventions, a small amount of runtime name rewriting, and the property protocol to communicate and control attribute visibility.
Python Doesn't Have True Access Modifiers
Python's design treats classes as agreements between developers rather than enforced contracts. Any attribute or method on an instance can be read or written from anywhere in the program, regardless of how it is named. This is a deliberate design choice: the language favors flexibility, duck typing, and trust over compile-time guarantees.
The practical consequence is that access control in Python is mostly about convention and documentation. When you see an attribute prefixed with an underscore, you are expected to treat it as internal. Nothing stops you from accessing it, but doing so means you are relying on implementation details that can change without notice.
The Underscore Conventions
Python's access model uses three levels expressed through naming:
| Convention | Example | Meaning |
|---|---|---|
| No prefix | self.username | Public: part of the stable interface |
| Single underscore | self._role | Protected: internal, not for external use |
| Double underscore | self.__password_hash | Private: name-mangled to avoid accidental access |
class User: def __init__(self, username, email): self.username = username self._role = "viewer" self.__password_hash = None def authenticate(self, password): return self._verify_password(password) def _verify_password(self, password): return password == "expected"
The single underscore is purely a convention. It has no runtime effect. Linters and IDEs will warn when you access _role from outside the class, but the interpreter will not complain. The double underscore, however, triggers name mangling, which changes how the attribute is stored.
How Name Mangling Works
When the interpreter encounters an attribute name that begins with two underscores and does not end with two underscores, it rewrites the name at compile time. The attribute __password_hash defined inside class User becomes _User__password_hash.
user = User("alice", "alice@example.com") print(user.__dict__) # {'username': 'alice', '_role': 'viewer', '_User__password_hash': None}
The purpose is not security. It is to prevent accidental overrides in subclasses and to make accidental external access less likely. The attribute remains fully accessible if you know the mangled name:
print(user._User__password_hash) # None
Name mangling applies only within the class body. A subclass that references self.__password_hash will have that reference rewritten to _Subclass__password_hash, which is a different attribute entirely. This is why double-underscore attributes are useful when you want to prevent a subclass from silently shadowing an attribute.
Using Properties for Controlled Access
When you need actual control over what happens when an attribute is read or written, properties are the standard mechanism. The @property decorator turns a method into a read-only attribute, and the setter decorator defines write behavior.
class User: def __init__(self, username): self.username = username self._password_hash = None @property def password_hash(self): return self._password_hash @password_hash.setter def password_hash(self, value): if not isinstance(value, str) or len(value) < 8: raise ValueError("Hash must be a non-empty string") self._password_hash = value
External code can now read and write user.password_hash, but the setter validates the value before storing it. This is the closest Python gets to a true accessor pattern. Properties are the right tool when you need validation, transformation, computed values, or a read-only interface.
Choosing Between Conventions and Properties
The choice depends on what you are protecting against.
Use a single underscore when the attribute is internal but you do not need interception logic. It documents intent without adding overhead. Most internal state in a class falls into this category.
Use a double underscore when you are building a class that will be subclassed and you want to prevent subclasses from accidentally overriding an attribute. This is the one case where name mangling provides real value beyond convention.
Use properties when you need validation, lazy computation, or a read-only interface. Properties also let you change an attribute's implementation later without breaking callers, because the public name stays the same.
Common Mistakes and Edge Cases
A frequent mistake is assuming name mangling provides security. It does not. Any developer can access the mangled attribute by writing the full mangled name. Name mangling is an accident-prevention mechanism, not an access-control mechanism.
Another edge case involves dunder methods. Names that begin and end with two underscores, such as __init__ or __str__, are not mangled. Mangling only applies when the name ends with at most one underscore.
class Example: def __init__(self): self.__value = 1 self.__value__ = 2 # not mangled e = Example() print(e.__dict__) # {'_Example__value': 1, '__value__': 2}
This distinction matters when you define special methods or attributes that must remain accessible under their exact name.
Runtime Behavior and Maintainability
At runtime, Python performs no access checks. Attribute lookup is resolved dynamically through __getattribute__ and __setattr__. Underscore conventions therefore have zero performance cost—the interpreter treats _role exactly like username.
Properties do add a small overhead because each access is a method call rather than a direct dictionary lookup. For most applications this is negligible. If you are accessing an attribute millions of times in a hot loop, a plain attribute is faster, but you should measure before optimizing.
From a maintainability perspective, the conventions matter most in a team setting. A clear naming policy—public attributes without prefix, internal attributes with a single underscore, and double underscores only when subclass collision is a real risk—keeps the codebase predictable. The Python standard library follows these conventions, so matching them makes your code consistent with the ecosystem.