October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
Dataclasses

Mastering Object-Oriented Programming (OOP) in Python

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The basic elements of OOP in Python are classes, objects (instances), attributes, methods, inheritance, composition, and protocols. They help you keep related state and behavior together—but Python does not require every problem to become a class. Use object-oriented design when it makes ownership, collaboration, or extension clearer; use functions and built-in data structures when they are simpler.

What classes and objects mean in Python

Strings, lists, dictionaries, and functions are already objects. A string has data and operations such as upper(); a list has operations such as append(). A class defines a new type. An instance is one object created from that type, carrying its own state and exposing behavior through attributes and methods.

Python’s tutorial describes classes as a way of “bundling data and functionality together.” That bundle is useful when several operations naturally work on the same state. It is not a rule that every real-world noun deserves a class.

Build a small class first

A task with state and behavior

class Task:
    def __init__(self, title):
        self.title = title
        self.completed = False

    def complete(self):
        self.completed = True

    def status(self):
        return "done" if self.completed else "open"

first = Task("Read about classes")
second = Task("Write an example")

first.complete()
print(first.status())   # done
print(second.status())  # open

Task defines the type, while first and second are separate instances. Each instance owns its own title and completed attributes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

__init__ initializes an instance that has already been created; it is not the allocation mechanism itself. Construction calls it automatically after Python creates the object.

Understand self, instance variables, and class variables

Why self appears in every instance method

When you call first.complete(), Python supplies first as the first argument to the method. self is the conventional name for that explicit parameter:

class Task:
    def complete(self):
        self.completed = True

self is not a keyword. Another name would work technically, but using self is the established convention and keeps code readable.

State unique to each instance

Assignments such as self.title create instance attributes. Changing one instance does not change another:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
first.title = "Read the Python tutorial"
print(second.title)  # Write an example

State deliberately shared by the class

A class attribute is looked up through the class and can be seen by all instances unless an instance attribute shadows it:

class Task:
    category = "general"  # one shared class attribute

    def __init__(self, title):
        self.title = title

Mutable class attributes are a common trap. This list is shared, not copied for each object:

class BadQueue:
    items = []

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

one = BadQueue()
two = BadQueue()
one.add("message")
print(two.items)  # ['message']

Create mutable state in __init__ when it belongs to each instance:

class Queue:
    def __init__(self):
        self.items = []

Encapsulation: clear interfaces, not enforced privacy

Encapsulation means keeping related state and operations behind an understandable interface. Python does not ordinarily prevent outside code from reading or changing an instance attribute. A leading underscore communicates that a name is a non-public implementation detail:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
class Account:
    def __init__(self, balance):
        self._balance = balance

    def deposit(self, amount):
        if amount < 0:
            raise ValueError("amount must not be negative")
        self._balance += amount

Code outside the class can still access account._balance; the underscore is a convention for callers and maintainers. A double-leading underscore triggers name mangling, which can avoid accidental collisions in subclasses. It is not security or true access control.

Duck typing and polymorphism through protocols

Depend on the operation you need

Python code can accept an object because it supplies the required behavior, without requiring a particular concrete class. This is often called duck typing: the contract is the set of operations the caller uses.

def show_first_line(source):
    line = source.read().splitlines()[0]
    return line

class MemorySource:
    def __init__(self, text):
        self.text = text

    def read(self):
        return self.text

class FixedSource:
    def read(self):
        return "Status: readynDetails follow"

print(show_first_line(MemorySource("HellonWorld")))
print(show_first_line(FixedSource()))

show_first_line needs an object with a read() method returning text. It does not need both classes to share a parent class. Keep the required operations small and document them clearly; duck typing is still a design contract.

Composition or inheritance?

Use the relationship and ownership questions below rather than treating one technique as universally superior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Question Composition Inheritance
Relationship “Has-a”: an object contains or collaborates with another object. “Is-a”: a subtype promises to behave as a kind of base type.
State ownership Each collaborator owns focused state. Base and subclass state are coupled through the hierarchy.
Extension Swap or add collaborators without changing the host’s type. Override or extend inherited methods.
Coupling Usually limited to a collaborator’s protocol. Depends on base implementation, method lookup, and substitution.
Lookup Delegation is explicit at the call site. Behavior may come from several classes and the method resolution order.

Composition: delegate to a collaborator

class EmailSender:
    def send(self, message):
        print(f"Email: {message}")

class Notifier:
    def __init__(self, sender):
        self.sender = sender

    def notify(self, message):
        self.sender.send(message)

notifier = Notifier(EmailSender())
notifier.notify("Backup complete")

Notifier has a sender. A test double or a different sender can provide the same send() operation without inheriting from EmailSender.

Inheritance: represent a genuine subtype

class Notification:
    def format(self, message):
        return message

class UrgentNotification(Notification):
    def format(self, message):
        return f"URGENT: {message}"

notice = UrgentNotification()
print(notice.format("Server unavailable"))

Inheritance is clearest when every subclass can genuinely stand in for the base type and the shared behavior belongs in that base. If the relationship is only “uses” or “contains,” composition is usually easier to reason about.

Overriding, super(), and method resolution order

Overriding a method

A subclass can replace an inherited method. Python searches the class and then its bases according to the method resolution order (MRO).

class Report:
    def render(self):
        return "report"

class HtmlReport(Report):
    def render(self):
        return f"<p>{super().render()}</p>"

print(HtmlReport().render())  # <p>report</p>

super() asks Python to continue lookup after the current class, rather than naming a particular parent directly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Multiple inheritance and cooperative calls

class Logged:
    def save(self):
        print("log")
        super().save()

class Stored:
    def save(self):
        print("store")
        super().save()

class Base:
    def save(self):
        print("finish")

class Record(Logged, Stored, Base):
    pass

Record().save()
print(Record.__mro__)

The MRO linearizes lookup through the hierarchy, including diamond-shaped relationships, while preserving ordering constraints and avoiding repeated processing of a base. Cooperative multiple inheritance works only when each participating method uses compatible signatures and calls super(). Inspect ClassName.__mro__ when lookup is difficult to follow; avoid deep or accidental hierarchies.

Special methods are language protocols

Special methods connect your objects to built-in operations and syntax. They are not arbitrary magic; each one follows a protocol expected by Python.

class Playlist:
    def __init__(self, songs):
        self._songs = list(songs)

    def __len__(self):
        return len(self._songs)

    def __iter__(self):
        return iter(self._songs)

    def __add__(self, other):
        return Playlist(self._songs + other._songs)

mix = Playlist(["A", "B"])
print(len(mix))
for song in mix:
    print(song)

__len__ enables len(mix), __iter__ enables iteration, and __add__ defines the meaning of + for two playlists. Implement a special method only when its behavior matches the operation’s normal expectations.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Dataclasses for record-like data

A dataclass is a normal Python class with generated support for common record operations. It is an idiomatic choice when the main purpose is grouping named data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    pages: int

book = Book("Effective Python", "Brett Slatkin",  Python)
print(book.title)

Use valid values when constructing the object—for example:

book = Book("Effective Python", "Brett Slatkin",  Python)

A dataclass does not create a separate kind of object; it uses ordinary class syntax and remains a Python class. Add methods or use a regular class when the type must enforce invariants, control mutation, coordinate collaborators, or protect a meaningful lifecycle. Choose field ownership and responsibilities explicitly rather than assuming the decorator solves design decisions.

When a function is better than a class

For one transformation with no lasting identity, a function and built-in data structures are often clearer:

def open_tasks(tasks):
    return [task for task in tasks if not task["completed"]]

tasks = [
    {"title": "Read", "completed": True},
    {"title": "Practice", "completed": False},
]
print(open_tasks(tasks))

A class becomes useful when the same state and operations must stay together, when invariants should be enforced at one boundary, or when interchangeable collaborators and subtype behavior make extension easier. Do not wrap a short, stateless operation in a class merely to use OOP.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A practical design exercise

  1. Choose a small workflow such as an order, library checkout, or notification system.
  2. List the state that must persist and decide who owns each piece.
  3. List operations callers actually need, then express small collaborator protocols where possible.
  4. Mark each type as data-focused (a possible dataclass), behavior-rich (an ordinary class), or unnecessary (a function plus built-in data).
  5. Sketch composition first, then consider inheritance only where a genuine subtype can honor the base contract.
  6. Compare the alternatives for coupling, substitutability, state ownership, extension, and method lookup. Keep the design whose responsibilities a new reader can explain.

The Bottom Line

Python OOP is a set of design tools, not a requirement. Start with the smallest useful class, keep instance and shared state intentional, depend on clear behavior-based protocols, and choose composition, inheritance, dataclasses, or plain functions according to the problem’s actual structure.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.