Somewhere around line 500, every script starts fighting back: loose functions everywhere, globals leaking state, data threaded through six layers of arguments. A Python class is the fix, one unit that keeps data and the functions that work on it together. This post walks through classes, objects, attributes, and methods, with runnable examples at every step.
“The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.”
Edsger Dijkstra
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 16 minutes
A Python class is how you bundle data and behavior into one neat package. Think of a class as a cookie cutter and objects as the cookies. The cutter (the class) decides the shape: what data each cookie carries and what each cookie can do. Each cookie (an object) is a real thing you can hold, with its own decorations. A class lists the attributes (data) and methods (functions) an object will have. An object is one specific thing built from that class. When you write my_dog = Dog("Rex", 5), Dog is the class and my_dog is the object.
Why does object-oriented programming (OOP) exist at all? Because real things have both state and behavior at the same time. A bank account has a balance (state) and it lets you deposit and withdraw (behavior). Tying that data and those actions together in one class keeps your code organized and reusable, and it is far easier to follow than a pile of loose functions and global variables. This post teaches you the basics with examples you can run yourself.
Part 2 starts here. You have spent 38 posts writing functions, wrangling lists, and catching exceptions. That works beautifully for scripts under 500 lines. But the moment your codebase grows, with several developers, features that lean on each other, and data that needs structure, plain functions start tripping over each other. A class gives you a single unit that holds the data and the functions that work on that data, all in one place. Every Python developer needs this skill, so let us build it from the ground up.
Table of Contents
Everything in Python Is Already an Object
Before you write your first class, here is a nice surprise: you have been using objects since the What is Programming tutorial. Every string, list, dictionary, and even every function in Python is an object. When you called "hello".upper(), you were calling a method on a string object. So OOP is not some new thing you have to bolt on. It has been there the whole time, a bit like discovering that every appliance in your house came off a factory blueprint: the blueprints existed all along, you just never saw them. Now you are simply learning to draw your own.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows how a class acts as a blueprint that creates multiple object instances, each with their own unique attribute values. The class Dog defines the structure (name, breed, age), and each time you call Dog(), Python creates a new object in memory with its own copy of those attributes. This blueprint-to-instance relationship is the foundation of object-oriented programming, and every class you write from this point forward follows this same pattern.
Let us prove the claim. In the snippet below we store the details of a student named Viraj in ordinary variables, then ask Python what each value really is.
📄 everything_is_object.py: proof that Python is objects all the way down
# Every value in Python is an object
name = "Viraj"
scores = [88, 92, 75]
info = {"name": "Viraj", "age": 24}
print(type(name)) # str is a class
print(type(scores)) # list is a class
print(type(info)) # dict is a class
print(type(42)) # Even integers!
print(type(print)) # Even built-in functions!
print(type(type)) # type itself is a class
▶ Output
<class 'str'> <class 'list'> <class 'dict'> <class 'int'> <class 'builtin_function_or_method'> <class 'type'>
What happened here: type() tells you the class behind any value. <class 'str'> means the string "Viraj" is an instance of the str class. Even type itself is a class, which is why type(type) comes back as <class 'type'>. Python really is objects all the way down. This is not a clever slogan; it is how the language is actually built, and it is the reason your own classes will fit right in.
Your First Class
Time to write your first Python class yourself. Think of __init__ as the registration desk at a pet daycare: the owner hands over the dog’s name, breed, and age, and the desk sets up a complete record before the dog walks in. That is exactly what happens every time you call Dog(...) below.
📄 first_class.py: the simplest possible 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}"
# Create objects (instances) from the class
buddy = Dog("Buddy", "Labrador", 3)
rex = Dog("Rex", "German Shepherd", 5)
print(buddy.bark())
print(rex.describe())
print(f"Are they the same object? {buddy is rex}")
▶ Output
Buddy says: Woof! Rex is a 5-year-old German Shepherd Are they the same object? False
What happened here: class Dog: defines the blueprint. __init__ is the constructor, the method that runs automatically the moment you create an instance. self refers to the specific object being built right then. buddy and rex are separate objects with their own data, yet they share the same methods. That is the whole idea in one line: one blueprint, many instances. And buddy is rex is False because they are two different objects in memory, even though they came from the same class.
Class vs Object: The Difference
Here is another way to picture it. A class is like the floor plan for an apartment building. The plan says every flat has two bedrooms, a kitchen, and a balcony. Each actual flat built from that plan is an object. The flats share the same layout, but the people, the furniture, and the paint colours inside each one are different. The plan is the class. Each real flat is an object. To see it in code, say three students named Aditi, Anvay, and Aviraj enroll in a course, and we want to check who is passing.
📄 class_vs_object.py: classes define structure, objects hold data
class Student:
def __init__(self, name, age, grade):
self.name = name
self.age = age
self.grade = grade
def is_passing(self):
return self.grade >= 50
# The class is Student
# These are three separate objects (instances)
aditi = Student("Aditi", 28, 92)
anvay = Student("Anvay", 26, 47)
aviraj = Student("Aviraj", 30, 88)
for student in [aditi, anvay, aviraj]:
status = "PASS" if student.is_passing() else "FAIL"
print(f"{student.name} (age {student.age}): {student.grade} - {status}")
▶ Output
Aditi (age 28): 92 - PASS Anvay (age 26): 47 - FAIL Aviraj (age 30): 88 - PASS
What happened here: One class, three objects. Each object carries its own name, age, and grade. The method is_passing() uses self.grade to check the grade of that specific student, not all students. That’s encapsulation in action. Data and behavior bundled together.
Attributes: Instance vs Class
Say two developers named Vinay and Anvi join the same company. Some facts belong to each of them alone, like their name and role. Other facts, like the company name, belong to everyone at once. Python models this split with instance attributes and class attributes.
📄 attributes.py: instance attributes belong to one object, class attributes to all
class Employee:
# Class attribute: shared by ALL instances
company = "TechnoScripts"
employee_count = 0
def __init__(self, name, role):
# Instance attributes: unique per object
self.name = name
self.role = role
Employee.employee_count += 1
def info(self):
return f"{self.name} ({self.role}) at {self.company}"
vinay = Employee("Vinay", "Backend Dev")
anvi = Employee("Anvi", "Frontend Dev")
print(vinay.info())
print(anvi.info())
print(f"Total employees: {Employee.employee_count}")
# Change the class attribute: it affects all instances
Employee.company = "TechnoScripts Pvt. Ltd."
print(vinay.info()) # Updated!
▶ Output
Vinay (Backend Dev) at TechnoScripts Anvi (Frontend Dev) at TechnoScripts Total employees: 2 Vinay (Backend Dev) at TechnoScripts Pvt. Ltd.
What happened here: company is a class attribute, so there is just one copy that every instance shares. When we changed it, both vinay and anvi saw the new value right away. name and role are instance attributes, so each object keeps its own. Changing Vinay’s name would not touch Anvi’s. The rule of thumb: use a class attribute for things that are truly shared, like a counter, a default, or the company name. Use instance attributes for anything that belongs to one object alone. A real-life version: the company name on everyone’s badge is shared, but the name printed on each badge is personal.
Methods: Your Object’s Actions
Say a customer named Prathamesh opens a bank account with 1000 in it. Every deposit and withdrawal should update his balance and leave a record behind. Here is that whole story written as a class.
📄 methods.py: methods that modify state and return data
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
self.transactions.append(f"+{amount}")
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError(f"Insufficient funds. Balance: {self.balance}")
self.balance -= amount
self.transactions.append(f"-{amount}")
return self.balance
def statement(self):
print(f"Account: {self.owner}")
print(f"Balance: {self.balance}")
print(f"History: {', '.join(self.transactions)}")
account = BankAccount("Prathamesh", 1000)
account.deposit(500)
account.withdraw(200)
account.deposit(1500)
account.statement()
▶ Output
Account: Prathamesh Balance: 2800 History: +500, -200, +1500
What happened here: Methods are functions that live inside a class. They always take self as their first parameter, and that is exactly how they know which object’s data to work with. deposit() changes self.balance for this one account, not for every account. You could open ten accounts, each with its own balance and transaction history, and they would all run the same methods without stepping on each other. Same buttons on the ATM, different balance behind each card.
The Four Pillars of OOP
OOP rests on four core ideas. Think of them as the four load-bearing walls of a house: each one holds up a different side, and together they keep a large codebase standing. You will learn each one in detail over the next several posts, but here is the roadmap so you know where you are headed:
The diagram shows the four pillars of object-oriented programming: encapsulation, inheritance, polymorphism, and abstraction, all sitting on top of classes and objects as the base. Each pillar solves a different problem. Encapsulation bundles data with the methods that use it. Inheritance lets related classes share behavior. Polymorphism lets different objects answer the same call in their own way. Abstraction hides the messy details behind a simple interface. You will explore each one in the posts ahead, so treat this as a preview, not a test.
The Catch: Mutable Class Attributes
Two students, Viraj and Rahul, each expect their own private grade list. Watch what actually happens when that list is declared at the class level.
📄 catch_mutable_class_attr.py: this catches everyone at least once
# WRONG: mutable class attribute shared between instances
class StudentBad:
grades = [] # This list is shared!
def __init__(self, name):
self.name = name
def add_grade(self, grade):
self.grades.append(grade)
s1 = StudentBad("Viraj")
s2 = StudentBad("Rahul")
s1.add_grade(95)
s2.add_grade(72)
print(f"Viraj's grades: {s1.grades}") # Surprise!
print(f"Rahul's grades: {s2.grades}") # Same list!
print(f"Same object? {s1.grades is s2.grades}")
▶ Output
Viraj's grades: [95, 72] Rahul's grades: [95, 72] Same object? True
What happened here: grades = [] written at the class level creates exactly ONE list that every instance shares. So when Viraj appends 95, Rahul sees it too, because both names point at the same list object. That is why s1.grades is s2.grades prints True. Picture one shared whiteboard in the office: anyone who writes on it changes what everyone else reads. The fix is simple: always put mutable attributes (lists, dicts, sets) inside __init__ as instance attributes, so each object gets a fresh one.
📄 catch_fix.py: the correct way
# CORRECT: each instance gets its own list
class StudentGood:
def __init__(self, name):
self.name = name
self.grades = [] # Instance attribute: unique per object
def add_grade(self, grade):
self.grades.append(grade)
s1 = StudentGood("Viraj")
s2 = StudentGood("Rahul")
s1.add_grade(95)
s2.add_grade(72)
print(f"Viraj's grades: {s1.grades}")
print(f"Rahul's grades: {s2.grades}")
▶ Output
Viraj's grades: [95] Rahul's grades: [72]
What happened here: Moving grades = [] into __init__ means Python runs that line fresh every time you create a student, so each object walks away with its own brand new list. Now Viraj’s grades and Rahul’s grades stay completely separate, which is exactly what you wanted. One small move, one big bug avoided.
When You’ll Use This
- Web applications: A
Userclass with login, logout, and profile methods. Each user is an instance with their own email, password hash, and session data. - APIs (Application Programming Interfaces): An
APIClientclass that stores the base URL and auth token. Each client instance points to a different service. - Games: A
Playerclass with health, inventory, and position. Each player is an independent object with its own state.
Common Mistakes
Mistake 1: Forgetting self in method definitions
📄 mistake_no_self.py
class Calculator:
# BAD: missing self
# def add(a, b):
# return a + b
# On Python 3.14.6 this raises:
# TypeError: Calculator.add() takes 2 positional arguments but 3 were given
# GOOD
def add(self, a, b):
return a + b
calc = Calculator()
print(calc.add(3, 5)) # 8
When you call calc.add(3, 5), Python quietly passes calc in as the first argument. If your method does not list self to catch it, Python ends up handing the method three values for two slots, and you get the TypeError shown in the comment above. The first parameter of every instance method has to be self.
Mistake 2: Modifying class attributes thinking they are instance attributes
We saw this in the Catch section. Here is the rule in one line. If the value is mutable (a list, dict, or set), put it in __init__. If it is immutable and genuinely shared (a company name or a counter), the class level is fine.
Mistake 3: Using a class when a dictionary would do
Do not create a class just because you can. If you are only storing data with no real behavior (no methods beyond __init__), a dictionary or a namedtuple is simpler and clearer. Classes earn their place once your objects actually do something.
Best Practices
- DO use PascalCase for class names (
BankAccount, notbank_account) - DO put mutable defaults in
__init__, never at class level - DO keep classes focused: one class, one responsibility
- DON’T forget
selfas the first parameter of instance methods - DON’T create classes for pure data: consider
dict,namedtuple, ordataclassinstead - DON’T put everything in one mega-class: if it does 10 unrelated things, split it
Conclusion
Let us pull it together. A Python class is a blueprint, and objects are the real things you build from it. Attributes store the data, methods define the behavior, and self ties both to one specific object. Class attributes are shared by everyone; instance attributes belong to a single object. The biggest catch is a mutable default at the class level, so always put your lists, dicts, and sets inside __init__.
You have just built your first classes. Next up, the constructors tutorial explores __init__ patterns: default arguments, validation, factory methods, and the difference between __init__ and __new__. And if you want to jump around or revisit earlier topics, the full index lives at the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Create Student with name, grade, and a pass check (grade >= 60).
- Exercise 2: Add a class method from string and a static validator.
- Exercise 3: Implement __str__, __repr__, __eq__, __lt__. Sort students by grade.
Frequently Asked Questions
What is the difference between a class and an object in Python?
A class is a blueprint or template that defines attributes and methods. An object (instance) is a specific entity created from that class with its own data. You can create many objects from one class, each with different attribute values. Think of the class as a cookie cutter and objects as cookies.
What does self mean in Python classes?
self is a reference to the current instance of the class. When you call obj.method(), Python automatically passes obj as the first argument (self). It’s how methods know which object’s data to work with. The name self is a convention, not a keyword, but you should never use anything else.
Why is everything an object in Python?
Python’s design philosophy is ‘everything is an object’, so integers, strings, functions, classes, and even modules are objects. This means they all have attributes and methods, can be passed to functions, stored in collections, and inspected with type(). This uniform design makes the language consistent and powerful.
When should I use a class vs a function in Python?
Use functions for stateless operations (input in, output out, no memory). Reach for a Python class when you need to maintain state across multiple operations, like a bank account that remembers its balance, or a game character that tracks health and inventory. If you have data plus multiple functions that operate on that data, a class bundles them together.
What is the difference between instance and class attributes?
Instance attributes are defined in __init__ using self.attr = value and each object gets its own copy. Class attributes are defined at the class level and shared by all instances. Use instance attributes for per-object data and class attributes for shared constants or counters.
Interview Questions on Python Classes and Objects
Interviewers rarely ask for definitions. They ask what happens in situations like these.
Q: How does Python decide between an instance attribute and a class attribute when you read self.x?
Python looks in the instance’s own __dict__ first, then falls back to the class, then to the parent classes in method resolution order. So an instance attribute always shadows a class attribute with the same name. This is also why reading emp.company works even though company was never set on the instance: the lookup falls through to the class.
Q: You built a Cart class for an online store, and items added to one customer’s cart suddenly appear in every other customer’s cart. What do you check first?
Check whether the items list was declared at the class level, like items = [] directly under class Cart:. That creates one shared list object that every instance appends to, which is exactly the symptom described. The fix is to move the assignment into __init__ as self.items = [], so each cart gets a fresh list. You can confirm the diagnosis quickly with cart_a.items is cart_b.items, which prints True when the list is shared.
Q: A teammate ran emp.company = "NewCorp" on one Employee instance expecting the company name to change for all employees, but only that one object changed. Why?
Assigning through an instance never modifies the class attribute. It creates a brand new instance attribute named company on that one object, which then shadows the shared class attribute for that object only. To change the value for everyone, assign through the class itself: Employee.company = "NewCorp". This asymmetry between reading (falls back to the class) and writing (creates an instance attribute) trips up a lot of developers.
Q: Your method call calc.add(3, 5) raises TypeError: Calculator.add() takes 2 positional arguments but 3 were given, yet you clearly passed only two arguments. What happened?
The method was defined without self, as def add(a, b):. When you call a method on an instance, Python automatically passes the instance itself as the first argument, so the method actually receives three values for two parameter slots. Adding self as the first parameter, def add(self, a, b):, fixes it. The mysterious extra argument in this kind of TypeError is almost always the instance.
Q: When would you reach for a dict, namedtuple, or dataclass instead of writing a regular class?
When the object is pure data with no real behavior. If the only method you would write is __init__, a dataclass gives you the constructor, __repr__, and __eq__ for free, and a namedtuple works well for small immutable records. A regular class earns its place once the data needs behavior attached to it, like validation, state changes, or business rules. Reaching for a full class too early just adds boilerplate.
Q: Two objects are created with identical arguments, like a = Dog("Rex", "GSD", 5) and b = Dog("Rex", "GSD", 5). What do a is b and a == b return, and why?
a is b returns False because is checks identity, and these are two separate objects at different memory addresses. a == b also returns False by default, because a plain class inherits __eq__ from object, which falls back to identity comparison. If you want value-based equality, define __eq__ on the class (or use a dataclass, which generates it for you).
Reference: the complete, always-current details live in the official Python documentation.
Related Posts
Previous: Python AI for Beginners: Call an Large Language Model (LLM) in 25 Lines
Next: Python: Constructors __init__ & Instance Variables
Series Home: Python + AI/ML Tutorial Series

No comment