Classes are one of the features Python beginners hear about early and put off learning for as long as possible. This tutorial explains what a class actually is, shows you the signals that tell you one is the right tool for a given job, and walks you through building one from the ground up.
Python lets you write code in several styles. You can write plain functions, use dictionaries to group data, or reach for a class. None of those choices is universally correct. The question is always: which one fits the problem in front of you right now? By the end of this tutorial you will have a clear answer for the class side of that question.
What a Class Actually Is
A class is a blueprint. When you write a class, you are describing a category of thing — not one specific thing, but a pattern from which individual things can be created. Those individual things are called instances.
Think of a class named Dog. The class itself is not a dog. It describes what every dog made from this blueprint will have: a name, a breed, and an age. Each time you create a new dog from that blueprint, you get one specific dog with its own name, breed, and age. That dog is an instance.
class Dog:
def __init__(self, name, breed, age):
self.name = name
self.breed = breed
self.age = age
def describe(self):
return f"{self.name} is a {self.age}-year-old {self.breed}."
# Create two separate instances from the same blueprint
dog_one = Dog("Biscuit", "Beagle", 3)
dog_two = Dog("Pepper", "Husky", 5)
print(dog_one.describe()) # Biscuit is a 3-year-old Beagle.
print(dog_two.describe()) # Pepper is a 5-year-old Husky.
A few things to notice in that code. The class keyword starts the definition. __init__ is a special method that runs automatically every time you create a new instance — it sets up the starting values. self is a reference to the specific instance being created or used. And describe is a regular method — a function that lives inside the class and has access to the instance's data through self.
__init__ is sometimes called a constructor in other languages. In Python the official term is initializer. Python calls it for you — you never call __init__ directly.
What self does
New learners frequently find self confusing. Here is the plain explanation: when Python runs dog_one.describe(), it automatically passes dog_one as the first argument to describe. The parameter that receives it is named self by convention. That is how the method knows whose data to use. You could technically name it anything, but self is the universal Python convention and you should use it.
Build the correct first line of an __init__ method for a class called Car that accepts make and year:
def, followed by the method name. __init__( opens the parameter list. self, must come first because Python passes the instance automatically. Then come the caller-supplied parameters make, and year):. The colon ends the signature and starts the method body. class and return do not belong here.
Classes vs. Functions vs. Dictionaries
Before deciding to write a class, it helps to know what the alternatives look like for the same problem. Consider a bank account with a balance, an owner name, and the ability to deposit and withdraw money.
The dictionary approach
# Dictionary approach — data and logic are separate
account = {"owner": "Maya", "balance": 500}
def deposit(account, amount):
account["balance"] += amount
def withdraw(account, amount):
if amount > account["balance"]:
print("Insufficient funds.")
return
account["balance"] -= amount
deposit(account, 200)
withdraw(account, 100)
print(account["balance"]) # 600
This works fine for one account. But notice that every function must receive the account dictionary as an argument. The data and the logic that operates on it are kept in separate places. For a single account in a short script, that is acceptable.
The class approach
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds.")
return
self.balance -= amount
def statement(self):
return f"{self.owner}'s balance: ${self.balance}"
# Each account is independent
account_a = BankAccount("Maya", 500)
account_b = BankAccount("Leo")
account_a.deposit(200)
account_a.withdraw(100)
account_b.deposit(1000)
print(account_a.statement()) # Maya's balance: $600
print(account_b.statement()) # Leo's balance: $1000
Now the data and the logic live together. Creating a second account does not require copying any functions — you just create a second instance. Each instance tracks its own balance independently.
The bundling of data and related behavior into one unit is called encapsulation. It is one of the core ideas behind object-oriented programming, and it is the main reason classes exist.
- Best when
- You have a single transformation or calculation with no state to carry between calls
- Limitation
- Must receive all context as arguments every time; does not remember anything between calls
- Best when
- You need to group a small set of related values and will not add behavior to them
- Limitation
- Logic that operates on the dictionary lives elsewhere; does not scale cleanly when you need multiple copies
- Best when
- You need state that persists, multiple independent copies of the same structure, or behavior bundled together with data
- Limitation
- Adds more structure than a short script may need; can feel like overkill for simple one-off tasks
The Counter class below is supposed to track a running total, but something is wrong. Click the line you think is the problem, then hit check.
count += 1 to self.count += 1. Without self., Python treats count as a local variable that does not exist yet inside the method, which raises an UnboundLocalError. The self. prefix is what connects a method back to the instance's stored data.
The Three Signals That Tell You to Use a Class
Learning to recognize when a class is the right tool is a practical skill. Here are three clear signals to look for.
Signal 1: You need to track state
State is information that needs to be remembered and potentially updated across multiple operations. If you find yourself passing the same variable into function after function just to keep it alive, that variable wants to live inside a class. The Counter example from the Spot the Bug exercise is the simplest version of this: a running total is state.
# State lives inside the instance — no need to pass it around
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
def reset(self):
self.count = 0
def value(self):
return self.count
c = Counter()
c.increment()
c.increment()
c.increment()
print(c.value()) # 3
c.reset()
print(c.value()) # 0
Signal 2: Several pieces of data naturally belong together
When you have a cluster of related variables that always travel together — say, a name, an email address, and a role — those variables are describing one thing. A class gives that thing a proper home and a name. It also means any function that works with a user just receives one user argument instead of three separate variables.
class User:
def __init__(self, name, email, role="viewer"):
self.name = name
self.email = email
self.role = role
def is_admin(self):
return self.role == "admin"
def summary(self):
return f"{self.name} ({self.email}) — {self.role}"
admin = User("Priya", "priya@example.com", "admin")
guest = User("Sam", "sam@example.com")
print(admin.summary()) # Priya (priya@example.com) — admin
print(admin.is_admin()) # True
print(guest.is_admin()) # False
Signal 3: You need multiple independent copies
If your program needs several separate versions of the same kind of thing — multiple players in a game, multiple sensors reporting data, multiple shopping carts — a class makes creating and managing each one straightforward. Each instance maintains its own data without any interference from the others.
class Player:
def __init__(self, name):
self.name = name
self.health = 100
self.score = 0
def take_damage(self, amount):
self.health = max(0, self.health - amount)
def earn_points(self, points):
self.score += points
def status(self):
return f"{self.name} — HP: {self.health} Score: {self.score}"
player1 = Player("Reza")
player2 = Player("Nadia")
player1.take_damage(30)
player1.earn_points(50)
player2.earn_points(80)
print(player1.status()) # Reza — HP: 70 Score: 50
print(player2.status()) # Nadia — HP: 100 Score: 80
Notice that player1.take_damage(30) only affected player1. player2's health is untouched. That independence is exactly what instances give you.
"Classes provide a means of bundling data and functionality together." — Python Software Foundation, Python 3 Documentation
How to Decide When to Use a Class in Python
Run through these five steps whenever you are unsure whether a class is the right choice for a piece of code you are writing.
-
Check whether you need to track state
Ask yourself whether the data you are working with needs to remember information between operations. If a function must repeatedly receive the same values as arguments just to carry them forward, a class is likely the better fit. State that changes over time — a balance, a score, a connection status — belongs inside an instance.
-
Check whether you have multiple related data fields
If the same cluster of variables travels through several functions together, those variables probably belong together inside a class as instance attributes. A
name,email, androlealways describing the same user is a clear example. Grouping them in a class makes your function signatures shorter and your code easier to read. -
Check whether you need multiple independent copies
If your program needs several separate versions of the same thing — multiple players, multiple accounts, multiple tasks — a class gives you a clean way to create and manage each one independently. Without a class, you end up with parallel lists of variables that drift out of sync.
-
Define the class and write
__init__Start with the
classkeyword and a name inCapWordsstyle. Write an__init__method that acceptsselfplus every value the instance needs at creation time. Assign each value toself.attribute_nameinside the body. These assignments are what create the instance attributes. -
Add methods that operate on the instance data
Write a method for every action that needs to read or modify the instance's attributes. Each method takes
selfas its first parameter. Access instance attributes withself.attribute_nameinside the method body. Keep each method focused on one responsibility so the class stays easy to reason about.
Python Learning Summary Points
- A class is a blueprint for creating objects called instances. Each instance holds its own copy of the class's data, called attributes, and can call the class's methods on that data.
- The
__init__method runs automatically when a new instance is created and sets up its starting attribute values.selfis a reference to the specific instance being worked with; it must be the first parameter of every instance method. - Reach for a class when you need to track state over time, when several related data fields travel together, or when your program needs multiple independent copies of the same structure. Plain functions and dictionaries remain the right choice when none of those conditions apply.
The most common mistake beginners make is treating classes as the default tool for everything. A function that does one thing cleanly is better than a class that does one thing verbosely. Use the three-signal test — state, grouped data, multiple copies — and you will pick the right tool far more consistently.
Frequently Asked Questions
A class in Python is a template for creating objects. It bundles together data (called attributes) and the functions that work on that data (called methods) into a single reusable structure. You define a class once and then create as many instances from it as you need.
Use a class when you need to track state across multiple calls, when you have several related pieces of data that naturally belong together, or when you need to create multiple copies of the same kind of thing — like multiple users or multiple game characters. A plain function is the right tool when you just need a single transformation with no ongoing state.
__init__ is the initializer method that Python calls automatically whenever you create a new instance of a class. It sets up the starting values for all the instance's attributes. You never call it directly — writing Dog("Biscuit", "Beagle", 3) is enough to trigger it.
self refers to the specific instance of the class that a method is being called on. When you call dog_one.describe(), Python passes dog_one as the first argument to describe, and that argument is received as self. It lets each instance keep track of its own data separately from all other instances.
No. A plain function or a dictionary is often the right tool. Classes shine when you have state that changes over time, multiple related data fields, and operations that belong logically together with that data. If none of those three apply, a simpler structure is likely the better choice.
An instance is a single object created from a class. If a class is a blueprint, the instance is the actual thing built from that blueprint. Each instance has its own copy of the class's attributes. Creating dog_one = Dog("Biscuit", "Beagle", 3) produces one instance; creating dog_two = Dog("Pepper", "Husky", 5) produces a second, completely independent one.
A class attribute is defined directly in the class body and is shared by all instances. An instance attribute is set inside __init__ using self and is unique to each individual instance. As a beginner, you will spend most of your time with instance attributes — they are what most descriptions of classes are talking about.
Yes. A class can have as many methods as needed. Each method is a function defined inside the class body and receives self as its first parameter so it can access the instance's data. The BankAccount example in this tutorial has three separate methods: deposit, withdraw, and statement.