PYTHON OOP · 2026

Complete Python OOP Guide

Master Object-Oriented Programming in Python with this comprehensive guide. Covers classes, inheritance, encapsulation, magic methods, ABCs, composition, dataclasses, MRO, and best practices with clean, Pythonic examples.

⏱ 35 min read 📅 Mar 2026 🎓 Intermediate 💻 Python

Advertisement

01 — OOP FUNDAMENTALS

Why OOP Matters in Python

Python's object-oriented programming model is elegant, powerful, and Pythonic. Unlike languages that force OOP, Python lets you choose your paradigm, but its core — everything from integers to modules — is built on objects. This guide walks you through OOP in Python with clarity and practical examples, covering classes, objects, inheritance, polymorphism, encapsulation, magic methods (dunders), class methods, static methods, abstract base classes, composition, and best practices used by professional Python developers.

Understanding OOP is essential for writing maintainable, scalable Python code. Whether you're building web applications, data processing pipelines, or AI systems, object-oriented design helps you model real-world entities and relationships naturally.

📦

Classes & Objects

The foundation of OOP: blueprints and instances. Learn how to define classes, create objects, and work with attributes and methods.

🧬

Inheritance & Polymorphism

Create class hierarchies, override behavior, and write flexible code that works with objects of different types seamlessly.

🔐

Encapsulation & Magic Methods

Protect internal state, control attribute access, and customize behavior with dunder methods to make objects Pythonic.

Advertisement

02 — CLASSES & OBJECTS

The Foundation of OOP

A class is a blueprint for creating objects. An object is an instance of a class, with its own data and behavior. Python's class statement defines both attributes (data) and methods (functions belonging to the class).

# Basic class definition class Dog: """A simple Dog class""" def __init__(self, name, age): self.name = name self.age = age def bark(self): return f"{self.name} says Woof!" def get_human_years(self): return self.age * 7 # Creating instances my_dog = Dog("Rex", 3) your_dog = Dog("Bella", 5) print(my_dog.bark()) print(f"{your_dog.name} is {your_dog.get_human_years()} in dog years")

03 — ENCAPSULATION

Controlled Access to Data

Python uses conventions rather than strict access control. A single underscore _protected means "internal use". Double underscore __private triggers name mangling to avoid accidental overriding. Properties with @property provide controlled access, a Pythonic alternative to getters/setters.

# Encapsulation with properties class BankAccount: def __init__(self, owner, balance): self.owner = owner self._balance = balance # protected convention self.__pin = "1234" # name mangled to _BankAccount__pin @property def balance(self): return self._balance @balance.setter def balance(self, amount): if amount < 0: raise ValueError("Balance cannot be negative") self._balance = amount def deposit(self, amount): if amount > 0: self._balance += amount return True return False acc = BankAccount("Alice", 1000) print(acc.balance) # property getter acc.balance = 1500 # property setter

04 — INHERITANCE & POLYMORPHISM

Building Class Hierarchies

Inheritance creates hierarchies; polymorphism allows interchangeable objects. Python supports multiple inheritance with clean MRO (Method Resolution Order). Subclasses can override or extend parent behavior.

# Inheritance and polymorphism class Animal: def __init__(self, name): self.name = name def speak(self): raise NotImplementedError("Subclass must implement") class Cat(Animal): def speak(self): return f"{self.name} says Meow!" class Dog(Animal): def speak(self): return f"{self.name} says Woof!" # Polymorphism in action animals = [Cat("Whiskers"), Dog("Buddy")] for animal in animals: print(animal.speak())

05 — MAGIC METHODS

Customizing Built-in Behavior

Magic methods define behavior for built-in operations: __str__, __len__, __add__, __eq__ and many more. They let your objects integrate seamlessly with Python syntax.

# Magic methods (dunders) class Vector: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"Vector({self.x}, {self.y})" def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __eq__(self, other): return self.x == other.x and self.y == other.y v1 = Vector(2, 3) v2 = Vector(1, 4) print(v1 + v2) # uses __add__ print(v1 == Vector(2, 3)) # uses __eq__

06 — CLASS VS STATIC METHODS

Methods That Belong to the Class

@staticmethod doesn't receive self or cls — acts like a regular function but lives in the class namespace. @classmethod receives the class as first argument, useful for factory methods.

# Class and static methods class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls, date_string): year, month, day = map(int, date_string.split('-')) return cls(year, month, day) # factory @staticmethod def is_valid_date(year, month, day): if month < 1 or month > 12: return False if day < 1 or day > 31: return False return True d = Date.from_string("2025-02-25") print(d.year) print(Date.is_valid_date(2025, 13, 1)) # False

07 — ABSTRACT BASE CLASSES

Enforcing Contracts with ABCs

The abc module lets you define abstract methods that must be implemented by subclasses. This enforces a contract and is ideal for frameworks.

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass @abstractmethod def perimeter(self): pass class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height) rect = Rectangle(5, 3) print(rect.area()) # 15

08 — BEST PRACTICES

Writing Pythonic OOP Code

Writing Pythonic OOP code means embracing Python's idioms rather than forcing patterns from other languages. Here are the key principles professional Python developers follow:

# Using __slots__ for memory efficiency class Point: __slots__ = ('x', 'y') # reduces memory footprint def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point({self.x}, {self.y})"

09 — COMPOSITION

Prefer Composition Over Inheritance

Composition means building complex objects from simpler, focused components. Instead of inheriting from a class to gain its behavior, you hold a reference to it. This creates has-a relationships and results in more flexible, loosely coupled code that is easier to test and maintain.

# Composition: has-a instead of is-a class Engine: def __init__(self, horsepower): self.horsepower = horsepower def start(self): return f"Engine with {self.horsepower}hp started" class GPS: def navigate(self, destination): return f"Navigating to {destination}" class Car: # Car HAS-A engine and HAS-A GPS def __init__(self, model, horsepower): self.model = model self.engine = Engine(horsepower) # composed self.gps = GPS() def drive_to(self, destination): print(self.engine.start()) print(self.gps.navigate(destination)) car = Car("Tesla", 450) car.drive_to("San Francisco")

10 — DATACLASSES

Boilerplate-Free Data Objects

Python 3.7+ introduced @dataclass — a decorator that auto-generates __init__, __repr__, and __eq__ from annotated fields. Dataclasses dramatically reduce boilerplate for classes that primarily hold data, while remaining fully compatible with regular class methods and inheritance.

from dataclasses import dataclass, field from typing import List @dataclass class Student: name: str age: int grades: List[float] = field(default_factory=list) def average(self) -> float: return sum(self.grades) / len(self.grades) if self.grades else 0.0 # __init__, __repr__, __eq__ auto-generated! s = Student("Alice", 20, [92.5, 88.0, 95.5]) print(s) # Student(name='Alice', age=20, ...) print(s.average()) # 92.0 print(s == Student("Alice", 20, [92.5, 88.0, 95.5])) # True

11 — __repr__ vs __str__

Representing Objects as Strings

__str__ is the human-friendly string, used by print(). __repr__ is the developer-friendly representation, ideally one you could paste back into Python to recreate the object. Always define __repr__ at minimum — if __str__ is absent, Python falls back to __repr__.

class Temperature: def __init__(self, celsius): self.celsius = celsius def __repr__(self): # Unambiguous: recreatable in Python return f"Temperature({self.celsius})" def __str__(self): # Human-friendly output return f"{self.celsius}°C ({self.celsius * 9/5 + 32:.1f}°F)" t = Temperature(100) print(t) # 100°C (212.0°F) — uses __str__ print(repr(t)) # Temperature(100) — uses __repr__

12 — METHOD RESOLUTION ORDER

Multiple Inheritance & MRO

Python resolves method calls in multiple-inheritance hierarchies using the C3 linearisation algorithm, also known as Method Resolution Order (MRO). Understanding MRO helps you predict which parent method gets called and design mixins safely. Use ClassName.__mro__ or help(ClassName) to inspect it.

# Mixins with MRO class JSONMixin: def to_json(self): import json return json.dumps(self.__dict__) class ValidateMixin: def validate(self): return all(v is not None for v in self.__dict__.values()) class User(JSONMixin, ValidateMixin): def __init__(self, name, email): self.name = name self.email = email u = User("Alice", "alice@example.com") print(u.to_json()) # {"name": "Alice", "email": "..."} print(u.validate()) # True # MRO: User → JSONMixin → ValidateMixin → object print(User.__mro__)

Advertisement

FAQ

Frequently Asked Questions

A class method receives the class as the first argument (cls) and can access or modify class state. A static method does not receive any special first argument and behaves like a regular function but belongs to the class namespace. Use @classmethod for factory methods, @staticmethod for utility functions.

Use composition when you need a "has-a" relationship rather than "is-a". Composition offers more flexibility, looser coupling, and often leads to more maintainable code. Favor composition over deep inheritance hierarchies to avoid complexity and promote code reuse.

Magic methods (dunder methods) are special methods with double underscores, like __init__, __str__, __add__. They define behavior for built-in operations and allow your objects to integrate seamlessly with Python syntax, enabling operator overloading and customizing object behavior.

__str__ returns a human-readable string, used by print(). __repr__ returns an unambiguous developer-facing string, ideally one that could recreate the object. Always implement __repr__ at minimum — if __str__ is missing, Python falls back to __repr__. In interactive sessions, __repr__ is shown by default.

Use @dataclass when your class primarily holds data with little or no complex logic. Dataclasses auto-generate __init__, __repr__, and __eq__, eliminating boilerplate. They also support immutability via frozen=True, ordering with order=True, and default factories. For classes with significant behavior or complex initialization, a regular class is often clearer.

MRO defines the order in which Python searches base classes when resolving a method call in a multiple-inheritance scenario. Python uses the C3 linearisation algorithm to compute a consistent, predictable lookup order. You can inspect it with ClassName.__mro__ or the mro() method. Understanding MRO is essential for safely composing mixins and avoiding the "diamond problem".

@property turns a method into a read-only attribute. Combine it with @attribute.setter and @attribute.deleter to add write and delete access. This lets you expose a clean attribute interface while running validation or computation under the hood, without breaking existing code that already uses the attribute. It is the Pythonic replacement for explicit getters and setters.

Stay in the Loop

Get new guides, code deep-dives, and Python insights delivered to your inbox.

No spam Free forever Unsubscribe anytime

Join readers learning Python & AI.

Python OOP Complete Guide - Built with data precision and Created through intensive information authentication. — By: Glenn Junsay Pansensoy | domain: code-sense.pansensoyglenn.workers.dev | © 2026