Encapsulation: Public, Protected, and Private Members
by Christoph Schiessl on Python
Learning this may surprise or even shock you: Python doesn't support truly private or protected class members. Therefore, encapsulation in the OOP sense is, strictly speaking, not supported.
What is Encapsulation?
Encapsulation is one of the pillars of Object-Oriented Programming: hiding a class's internal state (i.e., instance variables) from the outside so that the state cannot be directly accessed and mutated. This is important to protect the state from arbitrary, uncontrolled mutations. Usually, this is accomplished by forcing mutations to go through the class's methods, which can implement safeguards to allow only specific mutations and reject others. The way to enforce the usage of the class's methods is to make the state itself private or protected from the outside.
Public members are accessible from outside the class, and private members are only accessible from within the class itself. So, what are protected members? They are the middle ground because they are directly accessible from within the class itself and its subclasses. This is not true for private members because they cannot be directly accessed from within subclasses.
Public Attributes
You don't have to do anything special to get public members because this is the default behavior. For instance, the method A.foo()
is public, and therefore, it's accessible from every context.
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
... def foo(self):
... print("A.foo() has been called!")
... def bar(self):
... print("Calling A.foo() from within A ...")
... self.foo()
...
>>> class B(A):
... def baz(self):
... print("Calling A.foo() from within subclass B ...")
... self.foo()
...
>>> A().foo() # Calling A.foo() from the outside ...
A.foo() has been called!
>>> A().bar()
Calling A.foo() from within A ...
A.foo() has been called!
>>> B().baz()
Calling A.foo() from within subclass B ...
A.foo() has been called!
Protected Attributes
There's no mechanism in Python to make members protected, but there is a convention to prefix members with an _
to suggest that they are not supposed to be used from the outside. In the PEP8 section on naming styles, the underscore is referred to as a weak "internal use" indicator.
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
... def _foo(self):
... print("A._foo() has been called!")
... def bar(self):
... print("Calling A._foo() from within A ...")
... self._foo()
...
>>> class B(A):
... def baz(self):
... print("Calling A._foo() from within subclass B ...")
... self._foo()
...
>>> A()._foo() # Calling A.foo() from the outside ...
A._foo() has been called!
>>> A().bar()
Calling A._foo() from within A ...
A._foo() has been called!
>>> B().baz()
Calling A._foo() from within subclass B ...
A._foo() has been called!
As you can see, the leading _
doesn't affect privacy because, technically, the member is still public. It's just an agreed-upon convention without enforcement. The only effect that I'm aware of is for wildcard imports. Namely, statements such as from M import *
would not import identifiers with a leading underscore.
Private Attributes
Last but not least, you can also prefix members with two underscores to suggest that they are private, which results in their names getting mangled. But even in this case, they are technically still public and can be accessed from the outside, but you have to know how exactly name mangling works.
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
... def __foo(self):
... print("A.__foo() has been called!")
... def bar(self):
... print("Calling A.__foo() from within A ...")
... self.__foo()
...
>>> class B(A):
... def baz(self):
... print("Calling A.__foo() from within subclass B ...")
... self.__foo()
...
>>> A().__foo() # Calling A.foo() from the outside ...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__foo'. Did you mean: '_A__foo'?
>>> A().bar()
Calling A.__foo() from within A ...
A.__foo() has been called!
>>> B().baz()
Calling A.__foo() from within subclass B ...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in baz
AttributeError: 'B' object has no attribute '_B__foo'. Did you mean: '_A__foo'?
So, we can call A.__foo()
from within A
, but we get AttributeError
s if we try to call it from outside or a subclass of A
. Also, the error messages already suggest how mangling works: A member __foo
of a class A
is renamed to _A__foo
— knowing this, we can easily circumvent the effect of the leading __
.
Python 3.12.2 (main, Feb 17 2024, 11:13:07) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
... def __foo(self):
... print("A.__foo() has been called!")
... def bar(self):
... print("Calling A.__foo() from within A ...")
... self.__foo()
...
>>> class B(A):
... def baz(self):
... print("Calling A.__foo() from within subclass B ...")
... self._A__foo()
...
>>> print("Calling A.__foo() from the outside ..."); A()._A__foo()
Calling A.__foo() from the outside ...
A.__foo() has been called!
>>> A().bar()
Calling A.__foo() from within A ...
A.__foo() has been called!
>>> B().baz()
Calling A.__foo() from within subclass B ...
A.__foo() has been called!
Please don't confuse this with magic methods that must start and end with __
(e.g., __init__
).
Conclusion
As I said initially, strictly speaking, Python doesn't support private or protected members. Instead, there are conventions to express these concepts, but there is no enforcement. Also, as a developer, nobody forces you to follow the conventions. That said, I strongly advise that you follow the convention because not doing so will make it much harder for others to understand your code. Anyway, that's everything for today. Thank you for reading, and see you soon!