1. Home
  2. Blog
  3. Software Development

Software Development

Python Classes and Objects: OOP Tutorial With Real Examples

Learn object-oriented Python by building something real. Create classes, understand self and __init__, share behaviour with inheritance, protect data with properties, and know when a class is the wrong tool.

10 min read
Python classes and objects: OOP tutorial with real examples

Key takeaways

  • A class is a blueprint, and an object is one thing built from it.
  • __init__ sets up each new object, and self refers to that object.
  • Attributes set on the class are shared. Attributes set on self belong to one object.
  • Use @dataclass for data-holding classes, and prefer simple functions when there is no state to keep.

As a script grows, you end up passing the same group of variables between functions, and it becomes hard to tell which data belongs with which logic. Python classes and objects solve this by bundling data and behaviour into one unit.

In this tutorial we build a small bank account step by step, and add each OOP idea only when it solves a problem. We ran every example on Python 3.13, and the output shown comes from that run. If you are new to the language, read our Python tutorial for beginners first.

What are classes and objects?

A class describes what something is and what it can do. An object, also called an instance, is a specific one created from that description. “Account” is a class, while Asha’s account and Ben’s account are two objects.

Term Meaning Example
Class The blueprint Account
Object or instance One thing made from it a = Account("Asha")
Attribute Data on the object a.balance
Method A function on the class a.deposit(50)

You have been using objects all along. In Python, strings, lists and even functions are objects, which is why you can write "hello".upper().

Your first class

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be positive")
        self.balance += amount
        return self.balance

a = Account("Asha", 100)
b = Account("Ben")

a.deposit(50)
print(a.balance, b.balance)

# 150 0

__init__ runs automatically whenever you create an object, and it sets the starting state. Depositing into a did not touch b, because each object has its own data. Notice that the method also protects the balance from a bad value. Our guide to Python exception handling explains raise in more depth.

What is self?

self is simply the object that a method is working on. When you write a.deposit(50), Python effectively calls Account.deposit(a, 50), passing the object in as the first argument. That is how one method definition can serve every object.

The name self is a strong convention rather than a keyword, but always use it. Forgetting it in a method definition is the most common beginner error, and it produces a confusing message about the number of arguments.

Class attributes vs instance attributes

An attribute set directly in the class body is shared by all objects. One set on self belongs only to that object. This example shows how lookups behave:

class Account:
    interest_rate = 0.04    # class attribute

    def __init__(self, owner, balance=0):
        self.owner = owner  # instance attribute
        self.balance = balance

a = Account("Asha", 100)
b = Account("Ben")
print(a.interest_rate, b.interest_rate)   # 0.04 0.04

Account.interest_rate = 0.05
print(a.interest_rate, b.interest_rate)   # 0.05 0.05

a.interest_rate = 0.09
print(a.interest_rate, b.interest_rate, Account.interest_rate)
# 0.09 0.05 0.05

That last line surprises people. Assigning through a did not change the shared value. It created a new instance attribute on a that hides the class one. Python looks on the object first, then on the class.

Special (dunder) methods

Methods with double underscores let your objects work with Python’s built-in features. Without them, printing an account shows a useless memory address. Adding two gives it a readable form:

    def __str__(self):
        return f"{self.owner}: {self.balance}"

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

print(a)        # Asha: 150
print([a, b])   # [Account('Asha', 150), Account('Ben', 0)]
Method Triggered by
__init__ Creating an object
__str__ print(obj), str(obj)
__repr__ Debugging, or showing it in a list
__eq__ a == b
__len__ len(obj)
__add__ a + b

Inheritance

Inheritance lets a new class reuse an existing one and change only what differs. Here, Savings is an Account with an extra setting:

class Savings(Account):
    def __init__(self, owner, balance=0, limit=3):
        super().__init__(owner, balance)
        self.limit = limit

    def __str__(self):
        return f"{super().__str__()} (savings)"

s = Savings("Chitra", 500)
s.deposit(100)
print(s, isinstance(s, Account), s.limit)

# Chitra: 600 (savings) True 3

super() calls the parent’s version, so the setup logic is not repeated. deposit was inherited as it was, and __str__ was overridden to add a label. Only use inheritance for a real “is a” relationship. If a class merely needs something, such as a car needing an engine, give it that object as an attribute. This is known as composition, and it is usually more flexible than deep inheritance chains.

Polymorphism

Polymorphism means different objects can respond to the same method call in their own way. Calling code does not need to know the exact type:

class Square:
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side ** 2

class Circle:
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return round(3.14159 * self.radius ** 2, 2)

for shape in [Square(3), Circle(2)]:
    print(type(shape).__name__, shape.area())

# Square 9
# Circle 12.57

These two classes do not even share a parent. Python only cares that the object has an area method, an approach known as duck typing. Adding a Triangle later would need no change to the loop.

Encapsulation and properties

Python has no truly private attributes, and it relies on conventions instead. A single underscore, as in _price, tells other developers “this is internal, please do not touch”. To check values when they are set, use a property, which looks like a plain attribute from the outside:

class Product:
    def __init__(self, price):
        self.price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("price cannot be negative")
        self._price = value

p = Product(100)
p.price = 150
print(p.price)      # 150
p.price = -5        # ValueError: price cannot be negative

This means you can start with a plain attribute and add validation later without changing any code that uses the class. So do not write get_price() and set_price() methods out of habit, as you might in Java. The @ syntax here is a decorator, which we explain in Python decorators explained.

Class methods and static methods

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @classmethod
    def from_fahrenheit(cls, f):
        return cls(round((f - 32) * 5 / 9, 1))

    @staticmethod
    def is_freezing(celsius):
        return celsius <= 0

Temperature.from_fahrenheit(212).celsius   # 100.0
Temperature.is_freezing(-3)                # True
Type First argument Use it for
Instance method self Working with one object’s data
@classmethod cls Alternative constructors
@staticmethod None Helpers related to the class

Dataclasses

When a class mostly holds data, writing __init__, __repr__ and __eq__ by hand is tedious. @dataclass generates them from type annotations:

from dataclasses import dataclass, field

@dataclass
class Item:
    name: str
    price: int
    tags: list = field(default_factory=list)

print(Item("Pen", 10))
print(Item("Pen", 10) == Item("Pen", 10))

# Item(name='Pen', price=10, tags=[])
# True

Tip: Not everything needs a class. If it has no state and only one behaviour, a plain function is simpler. For simple lookups, a dictionary may be enough. Reach for a class when data and the rules that protect it clearly belong together.

Common mistakes

Using a mutable class attribute

This is a classic bug. The list is defined on the class, so all carts share it:

class Cart:
    items = []            # shared by every cart!

    def add(self, item):
        self.items.append(item)

c1, c2 = Cart(), Cart()
c1.add("pen")
print(c2.items)

# ['pen']

Create it in __init__ with self.items = [], so that every object gets its own list.

Other common errors

  • Forgetting self. Write self.balance, not balance, inside methods.
  • Forgetting super().__init__(). The child then misses attributes set up by the parent.
  • Building huge classes. A class that does everything is hard to test. Split it up by responsibility.
  • Deep inheritance. Two levels are usually plenty, and composition is often clearer.
  • Not writing tests. Classes with rules are easy to test, as our pytest tutorial shows using a small cart.

Final thoughts

Object-oriented Python comes down to grouping data with the behaviour that protects it. Start with __init__, methods and __str__, then add inheritance and properties when you hit a problem they solve. As a next step, take a script that passes the same few variables everywhere and turn them into one small class.

Frequently asked questions

What is the difference between a class and an object?

A class is the definition, or blueprint. An object is a specific instance created from it, with its own data. One class can create many objects.

What does __init__ do in Python?

It is the initializer, which runs automatically when you create an object. It usually sets the object’s starting attributes from the arguments you pass.

What is self in Python?

self refers to the object a method is called on. Python passes it automatically, and it lets the method read and change that object’s attributes.

What are the four pillars of OOP?

They are encapsulation, abstraction, inheritance and polymorphism. Together, they help you bundle data with behaviour, hide details and reuse code.

When should I use a class instead of a function?

Use a class when data and behaviour belong together across several operations. For one stateless operation, a plain function is simpler and easier to test.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →