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.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitches#1 Best Overall
__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:
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:
Rank #2
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:
Recommended Free Tools
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.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware match| 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.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →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.
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.
Best Value
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.
A practical design exercise
- Choose a small workflow such as an order, library checkout, or notification system.
- List the state that must persist and decide who owns each piece.
- List operations callers actually need, then express small collaborator protocols where possible.
- Mark each type as data-focused (a possible dataclass), behavior-rich (an ordinary class), or unnecessary (a function plus built-in data).
- Sketch composition first, then consider inheritance only where a genuine subtype can honor the base contract.
- 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.
Quick Recap
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.




