Python Object-Oriented Programming: Classes and Inheritance
Object-oriented programming is a paradigm that organizes code around objects — data structures that contain both data (attributes) and behavior (methods). Python supports OOP fully and, unlike some languages, does not force you to use it. You can write procedural Python, functional Python, or object-oriented Python depending on what fits your problem.
Python’s OOP is distinctive: everything is an object, classes are themselves objects (first-class citizens), and the type system uses duck typing — “if it walks like a duck and quacks like a duck, it is a duck.” This means you do not need formal interface contracts; objects are defined by what they can do, not what they inherit from.
Classes and Objects
A class is a blueprint. An object is an instance of that blueprint.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def description(self):
return f"{self.title} by {self.author}, {self.pages} pages"
book = Book("Sapiens", "Yuval Noah Harari", 443)
print(book.description())The class keyword defines the blueprint. The __init__ method is the constructor — Python calls it automatically when you create a new instance. The self parameter refers to the instance being created; it is always the first parameter of instance methods, though you never pass it explicitly. Naming it self is a strong convention — Python does not enforce this, but every Python programmer expects it.
Instance, Class, and Static Methods
Python classes support three types of methods, each serving a different purpose.
Instance Methods
Instance methods operate on a specific object instance. They receive self and have full access to the instance’s attributes and other methods. Most methods in a class will be instance methods — they represent the behavior of individual objects.
class Book:
def __init__(self, title):
self.title = title
def read(self): # Instance method
return f"Reading {self.title}"Class Methods
Class methods operate on the class itself, not on instances. They receive cls (the class) and are decorated with @classmethod. Use them for alternative constructors or operations that involve the class as a whole.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
@classmethod
def from_csv(cls, csv_line):
title, author = csv_line.strip().split(",")
return cls(title, author)The from_csv factory method creates a Book from a CSV string. If you later subclass Book, calling from_csv on the subclass returns an instance of the subclass — this is the advantage over a standalone function.
Static Methods
Static methods are utility functions that belong to the class’s namespace but do not access the instance or class. They are decorated with @staticmethod and behave like regular functions.
class Book:
@staticmethod
def is_valid_title(title):
return bool(title) and len(title) > 0Inheritance
Inheritance lets you define a class that extends or modifies another class. The child class inherits all attributes and methods from the parent and can override or extend them.
class LibraryItem:
def __init__(self, title, year):
self.title = title
self.year = year
def summary(self):
return f"{self.title} ({self.year})"
class Book(LibraryItem):
def __init__(self, title, year, author, pages):
super().__init__(title, year)
self.author = author
self.pages = pages
def summary(self):
base = super().summary()
return f"{base} by {self.author}, {self.pages} pages"
class DVD(LibraryItem):
def __init__(self, title, year, runtime):
super().__init__(title, year)
self.runtime = runtime
def summary(self):
base = super().summary()
return f"{base}, {self.runtime} min"super() gives you access to the parent class. You should always call super().__init__() in the child class to ensure the parent’s initialization runs. Method resolution order (MRO) determines which method is called when you invoke a method on an instance — Python uses the C3 linearization algorithm, which you can inspect with ClassName.__mro__.
Multiple Inheritance
Python supports multiple inheritance — a class can inherit from more than one parent. Use it sparingly; it adds complexity and can make code hard to follow.
class Flyer:
def fly(self):
return "Flying"
class Swimmer:
def swim(self):
return "Swimming"
class Duck(Flyer, Swimmer):
passWhen method names collide, Python resolves the conflict using MRO — the first class listed in the parentheses takes priority. The super() function follows the same MRO, so chained calls with super() in a multiple-inheritance hierarchy work predictably via the cooperative multiple inheritance protocol.
Encapsulation and Name Mangling
Python does not have strict private attributes. Convention and a language feature provide access control:
- Single underscore
_name: “protected” — a convention meaning “internal use, do not touch from outside.” It is not enforced by the interpreter. - Double underscore
__name: triggers name mangling. Python rewrites__nameto_ClassName__nameinternally. This prevents accidental overriding in subclasses but is not true privacy.
class Book:
def __init__(self, title, rating):
self.title = title
self._rating = rating # Protected convention
self.__isbn = None # Name-mangled
def set_isbn(self, isbn):
if len(isbn) == 13:
self.__isbn = isbnName mangling exists primarily to avoid accidental attribute collisions in inheritance hierarchies, not to enforce privacy. Python trusts programmers to respect conventions.
Polymorphism
Polymorphism means “many forms” — the same interface can work with different types. Python’s duck typing makes polymorphism natural: if an object has the method you call, it works, regardless of its type.
def print_summary(items):
for item in items:
print(item.summary())
# Works with any object that has a .summary() method
books = [Book(...), DVD(...), Magazine(...)]
print_summary(books)In statically typed languages, polymorphism requires inheritance or interface implementation. In Python, any object with a summary() method will work — no formal contract needed. This is powerful but requires discipline: name your methods consistently and document expected interfaces.
Dunder Methods
Dunder (double underscore) methods let your objects work with Python’s built-in operations. They are the hooks that make your objects behave like native Python types.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
return f"{self.title} by {self.author}"
def __repr__(self):
return f"Book({self.title!r}, {self.author!r}, {self.pages})"
def __len__(self):
return self.pages
def __eq__(self, other):
if not isinstance(other, Book):
return NotImplemented
return (self.title, self.author) == (other.title, other.author)| Method | Purpose |
|---|---|
__init__ | Constructor |
__str__ | Human-readable string (for print(), str()) |
__repr__ | Developer-readable string (for debugging) |
__len__ | Length (for len()) |
__eq__ | Equality comparison (==) |
__lt__ | Less than (<) |
__getitem__ | Indexing (obj[key]) |
__call__ | Make object callable (obj()) |
__enter__ / __exit__ | Context manager (with statement) |
Override __repr__ whenever you write a class — it makes debugging dramatically easier. The convention is that __repr__ should return a string that could recreate the object if passed to eval().
Composition vs Inheritance
Inheritance models an “is-a” relationship: a Book is a LibraryItem. Composition models a “has-a” relationship: a Library has Books. Favor composition over inheritance.
# Inheritance (is-a)
class Ebook(Book):
def __init__(self, title, author, format):
super().__init__(title, author)
self.format = format
# Composition (has-a)
class Library:
def __init__(self):
self.books = []
self.dvds = []
def add_book(self, book):
self.books.append(book)Inheritance creates tight coupling — changes to the parent class can break child classes. Composition is more flexible and easier to test. A good rule of thumb: if you are inheriting primarily to reuse code rather than to model a genuine subtype relationship, use composition instead.
Best Practices
Dataclasses for Simple Objects
Python 3.7+ dataclasses reduce boilerplate for classes that primarily hold data:
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
pages: int
year: int = 2024Dataclasses automatically generate __init__, __repr__, __eq__, and __hash__ based on the declared fields. Use them for value objects and data containers; use regular classes when you need custom behavior or inheritance-heavy designs.
Keep It Simple
- Prefer plain functions and modules over single-method classes
- Use properties (
@property) instead of getter/setter methods - Keep inheritance hierarchies shallow — no more than two or three levels
- Write small classes with a single responsibility
class Book:
@property
def age(self):
return 2024 - self.yearAbstract Base Classes
For defining interfaces that subclasses must implement, use abc:
from abc import ABC, abstractmethod
class LibraryItem(ABC):
@abstractmethod
def summary(self):
passRelated: Learn Python error handling and list comprehensions.
Composition Over Inheritance
The principle “favor composition over inheritance” advises building classes by combining objects rather than extending base classes. Composition provides more flexibility at runtime — you can swap components, add behaviors dynamically, and avoid the fragile base class problem where changes to a parent class break child classes. Django’s class-based views exemplify this with mixins, while the strategy pattern uses composition to swap algorithms at runtime.
Abstract Base Classes
Python’s abc module provides abstract base classes for defining interfaces. Decorate methods with @abstractmethod to require subclasses to implement them. ABCs can include concrete methods alongside abstract ones, providing shared functionality. The collections.abc module provides abstract base classes for containers, iterators, and sequences.
FAQ
What is the most important thing to remember? Focus on understanding the core concepts thoroughly before moving to advanced topics. Mastery comes from practice, not just reading.
How long does it take to learn this? The timeline varies by individual, but consistent daily practice of 30-60 minutes yields visible progress within weeks.
What are common mistakes beginners make? The most frequent errors include skipping fundamentals, not testing assumptions, and trying to learn too many things simultaneously.