The class is a blueprint, not the thing itself
Think of a cookie cutter and cookies. The cookie cutter is the blueprint. It defines the shape. You press it into dough and get a cookie. Then another. Then another. Every cookie came from the same cutter, so they all have the same shape, but each cookie is its own independent thing. You can put sprinkles on one without affecting the others.
A class is the cookie cutter. An instance is a cookie.
1class Dog:2 pass # an empty class for now34# Creating instances: call the class like a function5rex = Dog()6luna = Dog()7buddy = Dog()89# Three separate Dog objects10print(type(rex)) # <class '__main__.Dog'>11print(rex is luna) # False, completely different objects12print(type(rex) is type(luna)) # True, same class
You can create as many instances as you want. The class itself is defined once. Each instance gets its own independent data. Changing one does not touch the others.
The class name is capitalized by convention (Dog, not dog). This is not a rule Python enforces, but every Python codebase follows it. When you see a capitalized name being called like a function, you are looking at a class being instantiated.
__init__ and self
An empty class is not very useful. You need a way to give each instance its own data when it is created. That is what __init__ is for.
When you write Dog("Rex", "Labrador"), Python creates a new empty object and immediately calls __init__ on it. That method receives the new object as its first argument, which by convention is always named self. You use self to attach data to that specific instance.
1class Dog:2 def __init__(self, name, breed):3 self.name = name # attach 'name' to this specific Dog4 self.breed = breed # attach 'breed' to this specific Dog56rex = Dog("Rex", "Labrador")7luna = Dog("Luna", "Poodle")89print(rex.name) # Rex10print(luna.name) # Luna11print(rex.breed) # Labrador12print(luna.breed) # Poodle
What self actually is
In many languages, this (the equivalent of self) is hidden and automatic. Python makes it explicit. Every method on a class receives the instance as the first argument. Python passes it for you when you call the method on the instance.
1class Dog:2 def __init__(self, name):3 self.name = name45 def bark(self):6 print(f"{self.name} says: Woof!")78rex = Dog("Rex")910# These two lines do exactly the same thing11rex.bark() # Python passes rex as self automatically12Dog.bark(rex) # you pass rex explicitly
When you write rex.bark(), Python translates it to Dog.bark(rex) behind the scenes. The self parameter is not special syntax. It is just the first parameter, and Python fills it in for you. You could name it anything, but nobody does, because self is the universal convention.
Rule: every method in a class (including __init__) must have self as its first parameter. Forgetting it is the single most common mistake beginners make.
Instance attributes vs class attributes
Attributes set on self inside __init__ are called instance attributes. Every instance gets its own copy, separate from every other instance.
Attributes defined directly on the class body (not inside any method) are class attributes. They are shared across all instances.
1class Dog:2 species = "Canis lupus familiaris" # class attribute, shared by all34 def __init__(self, name):5 self.name = name # instance attribute, unique per dog67rex = Dog("Rex")8luna = Dog("Luna")910# Instance attributes are independent11print(rex.name) # Rex12print(luna.name) # Luna1314# Class attribute is the same for all15print(rex.species) # Canis lupus familiaris16print(luna.species) # Canis lupus familiaris17print(Dog.species) # Canis lupus familiaris (access directly on class)
Gotcha: mutable class attributes
Class attributes that are mutable objects (lists, dicts) are a trap. Because they are shared, modifying one instance's view of the attribute changes it for everyone.
1class Kennel:2 dogs = [] # shared list, BAD idea34 def add(self, dog):5 self.dogs.append(dog)67k1 = Kennel()8k2 = Kennel()910k1.add("Rex")11print(k1.dogs) # ['Rex']12print(k2.dogs) # ['Rex'] -- k2 sees it too! Same list object.
The fix: initialize mutable attributes inside __init__, not on the class body.
1class Kennel:2 def __init__(self):3 self.dogs = [] # each instance gets its own list45 def add(self, dog):6 self.dogs.append(dog)78k1 = Kennel()9k2 = Kennel()1011k1.add("Rex")12print(k1.dogs) # ['Rex']13print(k2.dogs) # [] -- k2 is untouched
Use class attributes for constants shared by all instances (like species above, or a tax rate, or a default timeout). Use instance attributes for anything that varies per object or could be mutated.
The four mistakes everyone makes at first
1. Forgetting self in the method signature
1class Dog:2 def __init__(self, name):3 self.name = name45 def bark(): # missing self6 print("Woof!")78rex = Dog("Rex")9rex.bark() # TypeError: bark() takes 0 positional arguments but 1 was given
Python passes the instance automatically, so the method needs a parameter to receive it. Every method needs self as the first parameter. No exceptions.
2. Accessing an attribute that does not exist yet
1class Dog:2 def __init__(self, name):3 self.name = name45 def get_owner(self):6 return self.owner # never set in __init__78rex = Dog("Rex")9print(rex.get_owner()) # AttributeError
Python does not pre-declare attributes. If you try to read self.owner before setting it somewhere, AttributeError is what you get. Set all attributes your object will ever use inside __init__, even if the initial value is None.
3. Calling a method without parentheses
1class Dog:2 def __init__(self, name):3 self.name = name45 def bark(self):6 print("Woof!")78rex = Dog("Rex")9rex.bark # no parens: this is the method object, not the call10rex.bark() # this actually calls it
Without parentheses you get a reference to the method, not its result. This often surfaces as a bug where a conditional like if rex.bark always evaluates to True because method objects are truthy, regardless of what the method would return if called.
4. Returning self when the method should return nothing
1class Counter:2 def __init__(self):3 self.count = 045 def increment(self):6 self.count += 17 return self.count # probably not what you want89c = Counter()10result = c.increment()11print(result) # 1 -- you got the count back, but why?12print(c.count) # 1 -- and the object still has the updated value
Methods that modify the object's state usually return None (no return statement). The caller reads the updated state by accessing the attribute afterward. Returning state values from mutating methods is not wrong, but it muddles the intention and can encourage callers to ignore the object and just work with the return value.
The exception is the builder pattern, where methods return self so you can chain calls. But that is a deliberate design choice, not an accident.