Skip to main content

Python Classes Explained

OOP Core

Defining Classes in Python

Classes package related data and behavior. Master syntax, attributes, and method definitions here.

Basics

A class is a blueprint: it describes what data its objects hold and what they can do. The class keyword introduces the definition, __init__ sets up each new object, and methods are ordinary functions written inside the class body:

class Invoice:
"""A bill sent to a customer."""

currency = "USD" # class attribute — one value shared by all invoices

def __init__(self, total: float, customer: str):
self.total = total # instance attributes — one set per object
self.customer = customer

def apply_discount(self, percent: float) -> None:
self.total *= 1 - percent
  • class keyword introduces the definition.
  • Methods accept self as the first argument (the instance).
  • Use docstrings to describe purpose and parameters.

Calling the class like a function creates an instance — a concrete object built from the blueprint:

inv = Invoice(200.0, 'Ada')

print(inv.total) # -> 200.0
print(inv.customer) # -> Ada
print(inv.currency) # -> USD — found on the class

inv.apply_discount(0.1)
print(inv.total) # -> 180.0

Each instance is independent: creating a second Invoice gives it its own total and customer, untouched by the first.

What self means

self is the instance the method was called on. Python passes it automatically: inv.apply_discount(0.1) is shorthand for calling the function on the class with the instance as the first argument. These two lines do exactly the same thing:

inv = Invoice(200.0, 'Ada')

inv.apply_discount(0.1) # what you write
Invoice.apply_discount(inv, 0.1) # what Python effectively runs

print(inv.total) # -> 162.0 — both calls ran, discounting twice

That is why every method signature starts with self — the function needs a name for "the object I am working on". The name is a convention, not a keyword, but everyone uses self; don't rename it.

Attributes

  • Instance attributes live on each object (self.total), created by assignment — usually in __init__.
  • Class attributes live on the class itself and are shared unless overridden.
  • Leverage @property to create computed attributes with read-only semantics.

Every object keeps its instance attributes in its own namespace; anything not found there is looked up on the class:

inv = Invoice(99.0, 'Grace')

print(inv.__dict__) # -> {'total': 99.0, 'customer': 'Grace'}
print('currency' in Invoice.__dict__) # -> True
print(inv.currency) # -> USD — falls back to the class

This two-level lookup is a common source of bugs with mutable defaults — see instance vs class variables for the details.

A class built step by step

Start with just the data. A bank account has an owner and a balance:

class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self.balance = balance

acct = BankAccount('Grace')
print(acct.owner, acct.balance) # -> Grace 0

Add behavior as methods. Because the balance lives on self, deposits and withdrawals can enforce rules in one place:

class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self.balance = balance

def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError('amount must be positive')
self.balance += amount

def withdraw(self, amount: float) -> None:
if amount > self.balance:
raise ValueError('insufficient funds')
self.balance -= amount

acct = BankAccount('Grace', 100)
acct.deposit(50)
acct.withdraw(30)
print(acct.balance) # -> 120

acct.withdraw(500)
# ValueError: insufficient funds

Finish with a __repr__ so the object prints usefully while debugging:

class BankAccount:
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self.balance = balance

def __repr__(self) -> str:
return f'BankAccount({self.owner!r}, balance={self.balance})'

print(BankAccount('Grace', 120)) # -> BankAccount('Grace', balance=120)

The pattern scales: data in __init__, rules in methods, presentation in dunder methods.

When to use a class

Reach for a class when data and the functions that operate on it belong together:

  • State plus rules — a balance that must never go negative, an order that moves through statuses.
  • Many similar objects — hundreds of accounts, each with the same behavior but its own data.
  • A type you want to nameisinstance(x, BankAccount) reads better than checking dict keys.

Skip the class when a plain dictionary or a couple of functions do the job: a one-off config mapping, a script that transforms data and exits. A class with no methods and no invariants is usually just a dict with extra steps — or a job for dataclasses.

type() and isinstance()

Every object knows its class, and isinstance checks membership including subclasses:

acct = BankAccount('Grace')

print(type(acct)) # -> <class '__main__.BankAccount'>
print(isinstance(acct, BankAccount)) # -> True
print(isinstance(acct, object)) # -> True — every class inherits from object
print(isinstance(True, int)) # -> True — bool is a subclass of int

Prefer isinstance(x, Cls) over type(x) == Cls in checks — the former keeps working when someone subclasses your type. More on the class/instance relationship in working with objects, and the full picture of the OOP toolkit lives in the OOP section.

Frequently Asked Questions

What does self mean, and why do I have to write it?

self is the instance a method is called on. Python turns inv.apply_discount(0.1) into Invoice.apply_discount(inv, 0.1) behind the scenes, so the method signature needs a parameter to receive that instance — that parameter is self. It is explicit in Python rather than implicit (like this in other languages), which is why forgetting it produces a TypeError about argument counts.

inv.apply_discount(0.1)
Invoice.apply_discount(inv, 0.1) # the same call, spelled out

What is the difference between a class and an object?

The class is the blueprint; an object (or instance) is one concrete thing built from it. BankAccount defines what every account has and can do; BankAccount("Grace", 100) creates one specific account with its own balance. One class, many independent instances.

When should I use a class instead of functions?

Use a class when several functions keep passing the same data around and that data has rules to enforce — bundle both into one type. Stick with plain functions and dicts for stateless transformations and one-off scripts. If you find yourself writing get_balance(account_dict) and withdraw(account_dict, amount), that is a class waiting to happen.

Next up in your learning path