8.1 Classes and Objects
A class is defined with the class keyword and typically includes an __init__ method — Python's constructor, called automatically the moment you create a new instance with ClassName(...).
Every instance method's first parameter is, by strong convention, named self — an explicit reference to the specific object the method was called on. This is a notable contrast with Java or C++, which hide this reference behind an implicit this; Python always makes it visible and explicit.
A Simple Class (from the Guide)
# A SIMPLE CLASS
class Car:
def __init__(self, color):
self.color = color # instance attribute
def drive(self):
print(f"{self.color} car driving")
c1 = Car("red")
c1.drive() # "red car driving"
8.2 Inheritance
A class inherits from another by naming it in parentheses: class Dog(Animal):.
The child class ("subclass") automatically receives every attribute and method the parent ("base class") defines, and it can selectively override any of them by simply redefining a method with the same name.
Inside an overriding method, calling super().method(...) reaches back and invokes the parent's original version — letting you extend inherited behavior instead of throwing it away completely.
# INHERITANCE IN ACTION
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self): # Overriding speak
return f"{self.name} says Woof!"
d = Dog("Buddy")
print(d.speak()) # "Buddy says Woof!"
8.3 Encapsulation Conventions
Python has no true "private" keyword the way Java does. Instead, privacy is a matter of convention rather than strict compiler enforcement.
The Single Underscore (_name)
A single leading underscore (_name) is a polite signal to other developers meaning "this is for internal use — please don't touch it from outside the class," but the language does not actually stop anyone from accessing it.
The Double Underscore (__name) & Name Mangling
A double leading underscore (__name) triggers something stronger called name mangling, where Python internally renames the attribute to _ClassName__name.
This makes accidental external access, or accidental overriding by a subclass, considerably less likely — though, notably, still not impossible for someone determined enough.
# Encapsulation and Name Mangling
class BankAccount:
def __init__(self, balance):
self._protected_balance = balance # polite convention
self.__secret_pin = 1234 # name-mangled
acc = BankAccount(5000)
print(acc._protected_balance) # 5000 (accessible, but frowned upon)
# print(acc.__secret_pin) # Raises AttributeError!
print(acc._BankAccount__secret_pin) # 1234 (reveals the mangled name!)
Pythonic Getters & Setters with @property
class Student:
def __init__(self, marks):
self._marks = marks
@property
def marks(self):
return self._marks
@marks.setter
def marks(self, val):
if val < 0 or val > 100:
raise ValueError("Marks must be between 0 and 100")
self._marks = val
8.4 Polymorphism & Magic (Dunder) Methods
Python largely achieves polymorphism through "duck typing" — summarized by the old saying:
"If it walks like a duck and quacks like a duck, treat it like a duck."
In practice, this means Python doesn't check an object's exact class before calling a method on it; it only cares whether the object actually has that method.
Dunder (Double Underscore) Magic Methods
Separately, dunder ("double underscore") methods such as __init__, __str__, __len__, and __add__ let your own custom classes hook directly into Python's built-in syntax.
Defining __add__ on a class, for example, lets you use the ordinary + operator on your own objects, and __str__ controls exactly what print() displays for an instance of that class.
# Operator Overloading with Dunder Methods
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # Overloads the + operator!
return Vector(self.x + other.x, self.y + other.y)
def __str__(self): # Controls print(v) representation!
return f"Vector({self.x}, {self.y})"
def __len__(self): # Enables len(v)
return int((self.x**2 + self.y**2)**0.5)
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2
print(v3) # "Vector(6, 8)"