Classes & Instances

beginner classes objects self __init__ attributes

A class is a blueprint for creating objects. Think of it like a cookie cutter — the class defines the shape, and every cookie we stamp out is an instance (an actual object in memory).

The class Keyword

We define a class with the class keyword. By convention, class names use PascalCase.

class Dog:
    species = "Canis familiaris"  # class attribute — shared by ALL dogs

    def __init__(self, name, age):
        self.name = name  # instance attribute — unique to each dog
        self.age = age

What Is __init__?

__init__ is the initializer, not the constructor. The actual constructor is __new__ (which creates the object). __init__ just sets up the initial state after the object already exists.

In simple language, __init__ is where we say “okay, this new object should have these attributes with these values.”

What Is self?

Every instance method receives self as its first argument. It’s a reference to the current instance. In simple language, self is how the object talks about itself — “my name”, “my age”.

Python passes self automatically — we never need to pass it ourselves.

class Dog:
    def __init__(self, name):
        self.name = name  # "my name is whatever was passed in"

    def bark(self):
        return f"{self.name} says Woof!"  # "my name says Woof!"

buddy = Dog("Buddy")
print(buddy.bark())  # Buddy says Woof!

Creating Instances

We call the class like a function. Python creates the object with __new__, then runs __init__ on it.

buddy = Dog("Buddy")
rex = Dog("Rex")

print(type(buddy))           # <class '__main__.Dog'>
print(isinstance(buddy, Dog))  # True

Instance vs Class Attributes

  • Class attributes live on the class itself. Every instance shares them.
  • Instance attributes live on each individual object. Each instance gets its own copy.
class Dog:
    species = "Canis familiaris"  # class attribute

    def __init__(self, name):
        self.name = name  # instance attribute

buddy = Dog("Buddy")
rex = Dog("Rex")

print(buddy.species)  # Canis familiaris — found on the class
print(rex.species)    # Canis familiaris — same shared value

Attribute Lookup Order

When we access obj.attr, Python looks in this order:

  1. The instance __dict__ (instance attributes)
  2. The class __dict__ (class attributes and methods)
  3. Parent classes (following the MRO)

If nothing is found anywhere, we get an AttributeError.

Attribute Lookup Order
1. Instance __dict__
buddy.__dict__ = {'name': 'Buddy'}
found? return
not found ↓
2. Class __dict__
Dog.__dict__ = {'species': 'Canis familiaris', ...}
found? return
not found ↓
3. Parent Classes (MRO)
Walks up the inheritance chain
found? return
not found ↓ AttributeError

Shadowing Class Attributes

When we assign to an instance, we create a new instance attribute that shadows the class one. The class attribute stays unchanged for everyone else.

buddy.species = "Robot Dog"  # creates instance attribute
print(buddy.species)  # Robot Dog — instance wins
print(rex.species)    # Canis familiaris — class attribute untouched

Peeking Inside: __dict__

Every object has a __dict__ that holds its attributes as a dictionary. This is super useful for debugging.

print(buddy.__dict__)  # {'name': 'Buddy', 'species': 'Robot Dog'}
print(Dog.__dict__)    # {'species': 'Canis familiaris', '__init__': ..., ...}

type() and isinstance()

  • type(obj) tells us the exact class of an object.
  • isinstance(obj, cls) checks if an object is an instance of a class or any of its subclasses. This is almost always what we want.
print(type(buddy))               # <class '__main__.Dog'>
print(type(buddy) == Dog)        # True
print(isinstance(buddy, Dog))    # True
print(isinstance(buddy, object)) # True — everything inherits from object

In simple language, a class is a template, and an instance is a real object built from that template. Python looks for attributes on the instance first, then the class, then parent classes. That lookup order is the key to understanding everything else in Python OOP.