Part of Python for Beginners

Python Classes & Objects Tutorial - __init__, self, inheritance, super() Explained

Sandy LaneSandy Lane

Video: Python Classes & Objects Tutorial - __init__, self, inheritance, super() Explained by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Python Classes and Objects: init, self, methods

class Name: defines a type. __init__(self, ...) is the constructor. self is the instance. Methods are functions whose first parameter is self. 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:

  1. Instance dict (obj.__dict__).
  2. Class dict (type(obj).__dict__).
  3. Parent classes (MRO).
  4. __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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.