Object-oriented programming in Python comes with two famous metaphors and a lot of jargon. The metaphors ("a class is a blueprint, an object is a house built from it") are accurate but don't tell you what's actually happening in memory or in your code. The jargon (encapsulation, inheritance, abstraction, polymorphism) sounds intimidating but each word describes one concrete thing you'll use daily. This article is the foundational primer that the PSF tutorial on classes assumes you'll read first: what a class IS, what an object IS, what self actually does, when to reach for OOP, and when to skip it entirely. It's one of 10 explainers in our Python OOP complete guide, the foundational entry that the other nine build on.
Key Takeaways
A class is a callable that creates objects. It bundles attributes (state) and methods (behavior) into a template.
An object is a bundle of state and behavior. Each one has its own attribute values; all instances of one class share the same methods.
self is just the first positional argument of every method. It's not a keyword; it's a convention that's universal in Python.
The four OOP principles are conventions in Python: encapsulation, inheritance, abstraction, polymorphism. Python applies them loosely — that's a feature.
Don't OOP everything. Short scripts, pure data transforms, and tiny containers stay simpler as functions or dataclass. Use OOP when state and behavior naturally bundle.
One class, two objects. The class is the template; each instance is filled in with its own state.
What a Class Actually Is
The shortest accurate definition: a class is a callable that creates objects. When you write class Dog: ... you're defining a callable named Dog. When you write rex = Dog("Rex", 4), you're calling it, and Python returns a new object that holds the values you passed.
That's it. Everything else is convenience syntax for organizing what the class produces.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print(f"{self.name}!")
def fetch(self, item):
print(f"{self.name} got {item}")
rex = Dog("Rex", 4) # Creates a Dog object
bella = Dog("Bella", 7) # Creates another Dog object
rex.bark() # Rex!
bella.bark() # Bella!
bella.fetch("ball") # Bella got ball
Two things happen in the line rex = Dog("Rex", 4):
Python creates a new empty object.
Python calls Dog.__init__(new_object, "Rex", 4), which assigns new_object.name = "Rex" and new_object.age = 4.
The __init__ method is the initializer (sometimes called the constructor, slightly inaccurately). It runs ONCE per object, right after creation, to set up the initial state. After __init__ returns, the new object becomes the value assigned to rex.
What an Object Actually Is
An object is a bundle of two things:
State — the values stored as attributes on the object (rex.name, rex.age).
Behavior — the methods the object has access to (rex.bark(), rex.fetch()).
Each Dog instance gets its own copy of the state. rex.name is "Rex" and bella.name is "Bella"; they don't interfere with each other. The behavior (the methods themselves) is shared: there's only ONE bark function in memory; both rex.bark() and bella.bark() call the same function, but Python passes the calling object as self, so the function reads each object's distinct state.
What self Actually Is
The most confusing thing for beginners, because it looks special but isn't. self is just the first positional argument of every method. The PSF tutorial says it directly: "self has no special meaning to Python."
When you call rex.bark(), Python translates that to Dog.bark(rex) internally. The rex object becomes the first argument; you named that argument self in the method definition. Same with rex.fetch("ball") — Python calls Dog.fetch(rex, "ball").
# These two lines do exactly the same thing:
rex.bark()
Dog.bark(rex)
# Both call the bark function with rex as the first argument.
You could legally name it anything (def bark(this):, def bark(me):, def bark(obj):) and it would work. But every Python codebase uses self by convention, so deviating breaks readability. Use self.
Why this matters. Beginners often write def bark(): (no self) and get TypeError: bark() takes 0 positional arguments but 1 was given. The "1 was given" is the implicit rex being passed. Once you understand self is just an argument, that error message becomes obvious.
The Four OOP Principles, Plainly
You'll hear these four words constantly. Each describes one concrete thing:
Encapsulation
Bundling state and behavior together so callers don't need to know the internals. Our Dog class encapsulates the name, age, and the bark/fetch methods. Code using Dog doesn't need to know HOW bark works; it just calls rex.bark().
Python doesn't enforce private attributes (you CAN read rex.name directly from outside the class). The convention is to prefix internal attributes with a single underscore (_internal_state) to signal "treat me as private." See the guide on @property for the controlled-access pattern.
Inheritance
One class building on another to reuse and extend. A Puppy class can inherit from Dog and add new behavior:
class Puppy(Dog):
def __init__(self, name, age, owner):
super().__init__(name, age)
self.owner = owner
def play(self):
print(f"{self.name} plays with {self.owner}")
The super().__init__ call runs the parent's initializer. See the dedicated guide on super().__init__() and MRO for the full mechanics.
Abstraction
Hiding complexity behind a clean interface. Users of your Dog class see bark() and fetch() but don't need to know what's inside. Python supports formal abstraction with abstract base classes (our ABC guide) and Protocols (PEP 544).
Polymorphism
Different objects responding to the same method call in their own way. Python handles this naturally via duck typing — if it has a bark() method, it's "bark-able," regardless of class:
class Cat:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} hisses (not really barking)")
for animal in [Dog("Rex", 4), Cat("Whiskers")]:
animal.bark() # Works for both
This is one of Python's biggest superpowers and the reason Python often skips formal interfaces. Pythonistas summarize it as "if it walks like a duck and quacks like a duck, it's a duck."
Common Beginner Mistakes
The six mistakes that show up consistently on Stack Overflow and the discuss.python.org "Help" category:
Mistake 1: Forgetting self
# Wrong
class Dog:
def bark(): # Missing self!
print("Woof!")
# Right
class Dog:
def bark(self):
print("Woof!")
Mistake 2: Mutable default arguments
The classic trap that catches everyone exactly once:
# Wrong — the list is shared across all instances!
class Bag:
def __init__(self, items=[]):
self.items = items
# Right
class Bag:
def __init__(self, items=None):
self.items = items if items is not None else []
# Class variable (shared)
class Dog:
species = "Canis lupus familiaris" # Shared by ALL dogs
def __init__(self, name):
self.name = name # Each dog's own name
species is on the class itself; every instance reads the same value. name is on each instance separately.
Mistake 4: Calling methods without instances
# Wrong
Dog.bark() # TypeError: missing required argument self
# Right
rex = Dog("Rex", 4)
rex.bark() # Python passes rex as self automatically
You can technically write Dog.bark(rex) but it's never the idiomatic form.
Mistake 5: Confusing __init__ with __new__
__init__ initializes an already-created object. __new__ actually creates it. Beginners almost never need __new__; if you do (custom metaclasses, immutable subclasses), see our metaclasses guide.
Mistake 6: Using OOP when functions would be simpler
A 50-line script that reads a CSV and prints a summary doesn't need a class. Functions and a dictionary are simpler:
One feature of Python that most tutorials skip but eventually matters: classes are themselves objects. When you write class Dog: ..., Python creates a class object and assigns it to the name Dog. You can pass that class object around, store it in lists, return it from functions:
def adopt(animal_class, name, age):
return animal_class(name, age)
rex = adopt(Dog, "Rex", 4) # animal_class IS Dog here
This matters for two things later: decorators (functions that take a class and return a modified class) and metaclasses (classes whose instances are classes). You can ignore both for now — see metaclasses explained when you're ready.
Where to Go Next
Now that the foundation is in place, the order most learners follow:
A class in Python is a callable that creates objects, defined with the class keyword. It bundles attributes (data) and methods (functions) into a single template. When you call the class (Dog('Rex', 4) for example), Python creates a new object that holds the specific values you passed, and that object has access to all the methods you defined inside the class. The class itself is also an object in Python — that's an unusual feature that becomes useful once you understand metaclasses, but you can ignore it for now.
What is an object in Python?
An object is a bundle of state (attributes) and behavior (methods) that lives in memory. When you write rex = Dog('Rex', 4), you create an object with two pieces of state (name='Rex', age=4) and access to all the Dog class's methods (bark, fetch, and so on). Each object has its own state, so rex and bella are two distinct objects even though they share the same class. In Python, almost everything you interact with is an object: numbers, strings, functions, modules, and even classes themselves.
What does self mean in Python?
self is just the first positional argument of every method. It's not a keyword. When you call rex.bark(), Python translates that to Dog.bark(rex) under the hood, passing the object as the first argument. The convention is to name that argument self, but you could legally name it anything; the convention exists because every Python tutorial and codebase uses it, so deviating breaks readability. The PSF tutorial says it clearly: "self has no special meaning to Python." You use it to access the object's own attributes from inside its methods (self.name, self.age).
What are the four principles of OOP?
Encapsulation (bundling state and behavior together so callers don't need to know the internals), inheritance (one class building on another to reuse and extend), abstraction (hiding complexity behind a clean interface so callers see only what matters), and polymorphism (different objects responding to the same method call in their own way). All four principles are present in Python but applied loosely: Python doesn't enforce private attributes (encapsulation is convention-based), supports inheritance with super().__init__() and MRO, supports abstraction with abstract base classes (ABC) and Protocols, and supports polymorphism naturally via duck typing.
When should I NOT use OOP in Python?
Skip OOP for: scripts under 100 lines doing one task (just write functions), pure data transformations (use list/dict comprehensions and functions), and small data containers (use dataclass or NamedTuple, which are OOP-lite). Reach for OOP when: state and behavior naturally bundle together (a Player class with health, position, and move methods), you have many similar entities sharing structure (User, AdminUser, GuestUser), or you're building something that other code will extend (a framework, a library, a plugin system). Python isn't OOP-only like Java; functions and OOP both have their place.
The Bottom Line: Class Makes Objects, Objects Bundle State and Behavior
A class is a callable that creates objects. Objects bundle state and behavior. self is the first positional argument. The four principles describe what OOP makes easy. Use OOP when state and behavior naturally bundle; skip it for short scripts and pure transforms. Once these five sentences feel obvious, you're ready for the rest of the series: the three method types, inheritance and MRO, the property decorator, magic methods, and eventually metaclasses. The full Python OOP guide indexes the next nine explainers.
Learn OOP With Hands-On Practice
CodeGym's Python track turns OOP into muscle memory through 800+ hands-on tasks across 62 levels, with structured coverage from classes and objects all the way to metaclasses and descriptors. The AI validator checks every solution; the AI mentor explains errors when you're stuck. You build real OOP intuition, not just textbook knowledge. First level free; full plan on the pricing page.
GO TO FULL VERSION