Python OOP: Classes, Inheritance, and Design Patterns
Object-oriented programming in Python is flexible by design. The official Python documentation describes classes as providing "a means of bundling data and functionality together" — but that single sentence understates how much flexibility Python gives you in deciding what to bundle, when, and how (source: docs.python.org/3/tutorial/classes.html). You can write simple classes that group data with behavior, or build complex hierarchies with descriptors, metaclasses, and protocol-based design. The key is knowing which tools to reach for and when composition beats inheritance.
This path covers OOP from the ground up: defining classes, understanding __init__ and self, choosing between class methods and static methods, designing with composition vs. inheritance, and using descriptors for attribute control. It also covers the distinction between data descriptors and non-data descriptors — a detail that controls attribute lookup priority in CPython and that shapes how property, classmethod, and staticmethod behave internally (source: docs.python.org/3/howto/descriptor.html).
Because Python is a multi-paradigm language — supporting object-oriented, procedural, and functional styles — this collection also covers when not to reach for OOP at all. Many Python programs are written entirely in procedural style, and knowing when a plain function is the right choice is part of writing clean Python.
Tutorials marked with the cert badge include a final exam that awards a certificate of completion you can download and share.
Python Classes and Objects Explained
Understand the class keyword, __init__, self, instance attributes, and methods -- the building blocks of object-oriented Python.
Python Class Example: Complete Guide
Building classes from scratch -- attributes, methods, __init__, __repr__, and organizing behavior around data.
What Is __init__ in Python?
How the constructor works, self explained, initialization vs creation, and common __init__ patterns.
Python OOP Methods Explained
Instance methods, class methods, static methods, and dunder methods -- when to use each and why.
classmethod vs staticmethod in Python
Side-by-side comparison with practical examples showing when each decorator is the right choice.
What Are Attributes in Python?
Instance vs class attributes, attribute lookup order, __dict__, and dynamic attribute patterns.
Composition vs Inheritance in Python
Why composition is often preferred, when inheritance makes sense, and refactoring from one to the other.
Python Descriptor Protocol
How descriptors power properties, class methods, and attribute access -- the mechanism behind Python's data model.
Python Descriptor Protocol and Attribute Lookup
The full attribute lookup chain: instance dict, class dict, descriptors, __getattr__, and MRO resolution.
Procedural Python Guide
When procedural style beats OOP, and how to write clean procedural code that doesn't need classes.
Python Is Multi-Paradigm: OOP, Procedural, and Functional
How Python supports multiple programming paradigms and how to choose the right one for your problem.
What Does Tightly Coupled Mean in Python?
What coupling is, how to spot tight coupling in your own classes, and how dependency injection and typing.Protocol loosen it.
How to Structure a Python Class from Scratch
Follow these steps to build a well-structured Python class. Each step corresponds to a distinct design decision — from naming your class to choosing where logic belongs.
-
Name your class using CapitalizedWords
Use the
classkeyword followed by a name written in CapitalizedWords (also called PascalCase). The name should describe what the class represents as a noun —BankAccount,HttpRequest,UserProfile. Avoid verb names and abbreviations. A clear noun name makes it immediately obvious what instances of this class model. -
Define __init__ to initialize instance attributes
Write an
__init__method that acceptsselfplus any data the object needs at creation time. Assign each piece of data toself.attribute_nameinside the method body. These become the instance attributes — values unique to each object created from the class. Keep__init__focused on initialization only; defer complex logic to separate methods. -
Add instance methods for object behavior
Define methods that describe what your object can do. Each instance method takes
selfas its first argument, giving it access to the object's own data. Name methods with verbs —calculate_total(),send_request(),validate(). Each method should do one thing. If a method grows beyond ten to fifteen lines, consider breaking it apart. -
Decide what belongs as a class attribute vs. an instance attribute
If a value is shared across every instance — a species name, a fixed configuration constant, a counter — define it as a class attribute directly on the class body, outside any method. If the value is unique to each instance, assign it inside
__init__usingself. Mixing these up is one of the most common sources of unexpected behavior in beginner OOP code. -
Choose between @classmethod, @staticmethod, and instance methods
Use an instance method (no decorator) when the method needs access to
self— the specific object's data. Use@classmethodwhen the method applies to the class as a whole and needs access tocls, such as alternative constructors. Use@staticmethodwhen the logic belongs in the class namespace conceptually but touches neither instance nor class state. Defaulting everything to instance methods is a common mistake — pick the right tool for what the method actually does. -
Add __repr__ for a useful string representation
Define a
__repr__method that returns a developer-readable string describing the object's current state. The convention is to return a string that, if evaluated, would recreate the object — for example,BankAccount(owner='Alice', balance=500). Without__repr__, printing or logging your object produces an unhelpful memory address. This is one of the dunder methods worth adding to nearly every class you write.
Frequently Asked Questions
Object-oriented programming (OOP) in Python is a design approach that organizes code into classes and objects. A class acts as a blueprint that defines attributes (data) and methods (behavior). You create instances of that class — called objects — each holding its own state. Python's OOP model supports the four core pillars: encapsulation, inheritance, abstraction, and polymorphism. Because Python is a multi-paradigm language, OOP is one option rather than a requirement, and you can mix it freely with procedural and functional styles.
A Python class is a reusable blueprint that defines the structure and behavior of objects. You define one using the class keyword, followed by the class name in CapitalizedWords convention, then a colon. The body contains an __init__ method to initialize attributes and any other methods that describe the object's behavior. Every instance method takes self as its first argument, which refers to the specific object the method is called on.
__init__ is Python's instance initializer method, called automatically when a new object is created from a class. It is not the constructor itself — that role belongs to __new__ — but it is where you set the initial values of instance attributes using self. Every attribute assigned inside __init__ is unique to that object instance, meaning two objects from the same class can hold completely different data.
A class method, decorated with @classmethod, receives the class itself as its first argument (conventionally named cls) rather than an instance. This makes it useful for alternative constructors or operations that apply to the class as a whole. A static method, decorated with @staticmethod, receives neither the instance nor the class — it is essentially a plain function that lives in the class namespace for organizational purposes. Use a class method when you need access to class-level data; use a static method when the logic belongs conceptually to the class but doesn't touch class or instance state.
Class attributes are defined directly on the class body, outside any method. They are shared across all instances, meaning every object created from that class sees the same value unless it is overridden on a specific instance. Instance attributes are set inside __init__ (or other methods) using self, and they belong exclusively to that object. Python's attribute lookup order checks the instance dictionary first, then the class dictionary, which is why setting an attribute on an instance shadows the class attribute without modifying it globally.
Composition means building a class by holding references to other objects rather than inheriting from them. Use composition when the relationship between two classes is "has a" rather than "is a" — for example, a Car has an Engine rather than being one. Composition is generally preferred because it keeps classes loosely coupled, makes the code easier to test in isolation, and avoids the fragile base class problem that deep inheritance hierarchies create. Inheritance still makes sense when you have a genuine subtype relationship and want to extend or override specific behavior.
The descriptor protocol is the mechanism Python uses to customize attribute access. Any class that defines __get__, __set__, or __delete__ is a descriptor. When a descriptor object is assigned as a class attribute, Python calls those methods instead of doing a plain dictionary lookup whenever the attribute is accessed, set, or deleted on an instance. This is how property, classmethod, staticmethod, and slot-based attributes all work internally.
Descriptors that define __set__ or __delete__ are classified as data descriptors and take priority over an instance's own __dict__. Descriptors that only define __get__ are non-data descriptors and can be overridden by instance attributes. Python 3.6 added __set_name__, which is called at class creation time and lets a descriptor know which attribute name it was assigned to — useful for auto-naming internal storage. Understanding the full protocol is essential for writing advanced Python that controls attribute behavior at the class level.
No. Python is a multi-paradigm language that supports object-oriented, procedural, and functional programming styles. While everything in Python is technically an object — including functions, modules, and primitive values — you are not required to use classes or OOP patterns in your code. Many Python programs and scripts are written entirely in procedural style with plain functions and no class definitions. Choosing when to use OOP versus a simpler procedural approach is one of the key design decisions covered across the tutorials in this collection.
Sources & References
- Python Software Foundation. Classes. Python 3 Documentation. docs.python.org/3/tutorial/classes.html
- Python Software Foundation. Descriptor HowTo Guide. Python 3 Documentation. docs.python.org/3/howto/descriptor.html
- Python Software Foundation. abc — Abstract Base Classes. Python 3 Documentation. docs.python.org/3/library/abc.html
- van Rossum, G. & Talin. PEP 3119 — Introducing Abstract Base Classes. Python Enhancement Proposals. peps.python.org/pep-3119/