Encapsulation & Dunder Methods
Naming conventions for 'private' attributes, @property, and hooking into Python's built-in syntax.
Python doesn't have true private attributes enforced by the language — it relies on naming convention instead:
self.name— public, use freely.self._name— "protected" by convention: a signal to other developers "internal, don't touch directly," but nothing stops access.self.__name— name-mangled: Python internally renames it to_ClassName__name, which makes accidental external access unlikely (though not impossible).
class YouTuber:
def __init__(self, name, subscribers):
self.name = name
self.__subscribers = subscribers # name-mangled
def get_subscribers(self):
return self.__subscribers
munna = YouTuber("CodeWithMunnaX", 250_000)
print(munna.get_subscribers()) # 250000
# print(munna.__subscribers) # AttributeError — it's actually stored as _YouTuber__subscribers
@property — controlled access that still looks like an attribute
A property lets you run code on attribute access/assignment while callers still use plain attribute syntax (obj.subscribers, not obj.get_subscribers()).
class YouTuber:
def __init__(self, name, subscribers):
self.name = name
self._subscribers = subscribers
@property
def subscribers(self):
return self._subscribers
@subscribers.setter
def subscribers(self, value):
if value < 0:
raise ValueError("Subscribers can't be negative")
self._subscribers = value
munna = YouTuber("CodeWithMunnaX", 250_000)
print(munna.subscribers) # calls the getter, looks like a plain attribute
munna.subscribers = 260_000 # calls the setter, validated
Dunder ("double underscore") methods
Methods like __str__, __eq__, __len__ hook your class into Python's built-in syntax and functions — this is called operator overloading.
class YouTuber:
def __init__(self, name, subscribers):
self.name = name
self.subscribers = subscribers
def __str__(self):
return f"{self.name} ({self.subscribers:,} subs)"
def __eq__(self, other):
return self.name == other.name
def __len__(self):
return self.subscribers
munna = YouTuber("CodeWithMunnaX", 250_000)
print(munna) # uses __str__, not the default <...object at 0x...>
print(len(munna)) # uses __len__ -> 250000
print(munna == YouTuber("CodeWithMunnaX", 0)) # uses __eq__ -> True, same name
Without __str__, print(munna) would show something unhelpful like <__main__.YouTuber object at 0x7f...> — defining it is one of the first things worth adding to any class you want to debug or display easily.
- •_name signals 'internal, don't touch' by convention only; __name is name-mangled to _ClassName__name, making outside access harder but not impossible.
- •@property lets a method be accessed like a plain attribute; pairing it with @x.setter lets you validate assignments too.
- •Dunder methods (__str__, __eq__, __len__, ...) hook your class into built-in syntax — print(), ==, len() all check for them.
- •Defining __str__ is one of the highest-value small additions to any class — it makes print(obj) actually useful for debugging.
- •Python has no true private attributes enforced by the language — encapsulation here is convention plus name-mangling, not a hard restriction.
@property with validation, and dunder methods
Revision Flashcards
Tap a card to flip it and see the answer.