Python Classes & Objects Tutorial - __init__, self, inheritance, super() Explained
Video: Python Classes & Objects Tutorial - __init__, self, inheritance, super() Explained by Taught by Celeste AI - AI Coding Coach
Python Classes and Objects: init, self, methods
class Name:defines a type.__init__(self, ...)is the constructor.selfis the instance. Methods are functions whose first parameter isself. Objects bundle data (attributes) with behavior (methods).
Object-oriented programming groups state and the operations on that state into a single unit. Python's class keyword is the entry point.
Defining a class
class Dog:
def __init__(self, name, breed, age):
self.name = name
self.breed = breed
self.age = age
def bark(self):
return f"{self.name} says Woof!"
def describe(self):
return f"{self.name} is a {self.age} year old {self.breed}"
class Dog: declares a new type. By convention, class names use CamelCase.
__init__ is the constructor — runs when you create a new Dog. It takes self (the new object) and the constructor arguments.
self.name = name stores name as an attribute on the new object.
Creating objects
rex = Dog("Rex", "German Shepherd", 5)
luna = Dog("Luna", "Golden Retriever", 3)
print(rex.describe()) # Rex is a 5 year old German Shepherd
print(rex.bark()) # Rex says Woof!
Dog(...) calls __init__ and returns a new instance. Attributes are accessed with .: rex.name, rex.age.
Methods are called with .: rex.bark(). Python passes rex automatically as self.
What is self?
self is the convention — it's just the first parameter of every instance method, bound to the object.
rex.bark()
# is equivalent to:
Dog.bark(rex)
Python's first-arg-as-instance is explicit (compared to JavaScript's this or Java's implicit context). You see exactly when self is the receiver.
You can name it anything — me, this, obj — but self is universally expected. Use it.
Modifying attributes
luna.age = 4
luna.color = "golden" # adds a new attribute
del luna.color # removes it
Python doesn't enforce a fixed schema — you can add or remove attributes any time. (Use __slots__ if you want to lock down attributes; covered later.)
Class attributes vs instance attributes
class Student:
count = 0 # class attribute — shared
def __init__(self, name, grade):
self.name = name # instance attribute — per object
self.grade = grade
Student.count += 1
a = Student("Alice", "A")
b = Student("Bob", "B")
print(Student.count) # 2
print(a.count) # 2 (same)
count lives on the class itself. All instances see the same value.
self.name is per-instance — a.name and b.name are independent.
Beware: self.count = 5 creates a new instance attribute that shadows the class attribute. To increment a shared counter, always use Student.count += 1 or type(self).count += 1.
Special methods: str and repr
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __str__(self):
return f"{self.name} (Grade: {self.grade})"
def __repr__(self):
return f"Student('{self.name}', '{self.grade}')"
alice = Student("Alice", "A")
print(alice) # Alice (Grade: A) — uses __str__
print(repr(alice)) # Student('Alice', 'A') — uses __repr__
print([alice]) # [Student('Alice', 'A')] — list uses __repr__
__str__ is for users — friendly output. __repr__ is for developers — ideally enough to reconstruct the object.
If you only define one, do __repr__ — Python falls back to it for str() if __str__ is missing.
classmethod and staticmethod
class Student:
count = 0
def __init__(self, name, grade):
self.name = name
self.grade = grade
@classmethod
def from_string(cls, data):
name, grade = data.split(",")
return cls(name.strip(), grade.strip())
@staticmethod
def is_passing(grade):
return grade in ["A", "B", "C"]
# Alternative constructor
bob = Student.from_string("Bob, B")
# Pure utility — no instance, no class needed
print(Student.is_passing("D")) # False
@classmethod receives cls (the class) instead of self. Used for alternative constructors (Student.from_string).
@staticmethod receives nothing special — it's just a function namespaced inside the class. Use when the function logically belongs to the class but doesn't need access to self or cls.
For most behavior, instance methods (self) are the right choice.
A pet shelter (preview of inheritance)
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
self.adopted = False
def speak(self):
return "..."
def info(self):
status = "Adopted" if self.adopted else "Available"
return f"{self.name} ({self.age}yr) - {status}"
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age)
self.breed = breed
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def __init__(self, name, age, color):
super().__init__(name, age)
self.color = color
def speak(self):
return f"{self.name} says Meow!"
class Dog(Animal): makes Dog a subclass of Animal. super().__init__(...) calls the parent's constructor.
Each subclass overrides speak(). Lesson 20 covers inheritance and polymorphism in depth.
Attribute access protocol
When you write obj.x, Python looks for x in this order:
- Instance dict (
obj.__dict__). - Class dict (
type(obj).__dict__). - Parent classes (MRO).
__getattr__if defined.
If x isn't found anywhere, raises AttributeError.
print(obj.__dict__) # {'name': 'Rex', ...} — instance attrs
print(Dog.__dict__) # methods, class attrs
Methods are just functions
class Dog:
def bark(self):
return "Woof"
d = Dog()
print(d.bark) # <bound method Dog.bark of <Dog object>>
print(Dog.bark) # <function Dog.bark>
# Same call, two ways:
d.bark() # idiomatic
Dog.bark(d) # explicit; works the same
d.bark is a "bound method" — bark plus d baked in as self.
Common stumbles
Forgetting self. def bark(): ... errors when called as a method. Every instance method needs self as the first parameter.
Mutable class attribute as state.
class Cart:
items = [] # shared across all carts!
c1 = Cart()
c1.items.append("apple")
c2 = Cart()
print(c2.items) # ["apple"] — bug!
Use __init__ to create per-instance lists: self.items = [].
Calling parent method without super().. Skips MRO; in multiple inheritance, breaks subtly. Use super().method().
Defining __init__ without storing args. def __init__(self, name): pass — the name is lost.
Mutating self in __init__ and forgetting to assign. self.items.append(...) requires self.items to already exist.
Comparing instances. Without __eq__, comparison is by identity (memory address). Dog("Rex") == Dog("Rex") is False unless you implement __eq__.
What's next
Lesson 20: inheritance, polymorphism, duck typing. Subclasses, method overriding, super(), and Python's "if it looks like a duck" approach.
Recap
class Name: defines a type. __init__(self, ...) is the constructor; self is the instance. Attributes via self.x = ...; class attributes are shared across instances. __str__ for user-friendly output, __repr__ for debugging. @classmethod for alternative constructors; @staticmethod for namespaced utilities. Inherit with class Sub(Base): and call super().__init__(...).
Next lesson: inheritance and polymorphism.