Python: Object-Oriented Programming
Move beyond functions and data into modeling real-world concepts with classes and objects. OOP is the dominant paradigm in professional Python and the foundation of most large codebases.
What is OOP?
Object-Oriented Programming is a way of structuring code around objectsrather than around functions and logic. An object bundles related data (attributes) and the operations that act on that data (methods) into a single unit. The goal is to model the real world more naturally: a bank account, a user profile, or a network connection are all things with state and behavior that belong together.
OOP rests on four core pillars. Encapsulation hides internal details and exposes only what is necessary. Inheritance allows new classes to build on existing ones, reusing and extending their behavior. Polymorphismlets different objects respond to the same interface in their own way.Abstraction simplifies complexity by exposing only relevant information.
Classes, Objects, and __init__
A class is a blueprint. An object (instance) is a concrete thing built from that blueprint. The__init__ method is the constructor. Python calls it automatically whenever you create a new instance, giving you the chance to set its initial state. The first parameter,self, always refers to the instance being created. It is not a keyword; it is a convention, but one that every Python developer honours.
Defining and Instantiating a Class
PythonA blueprint and two objects built from it.
__str__ and __repr__
By default, printing an object produces something like<Dog object at 0x...>, which is useless.__str__defines the human-readable string shown byprint().__repr__provides an unambiguous technical representation used in the REPL and during debugging. A good rule of thumb: __repr__should ideally be a valid Python expression that could recreate the object.
String Representations
PythonOne for humans, one for developers.
Encapsulation: Private and Protected Members
Encapsulation means keeping internal state hidden from external code that does not need to know about it. Python enforces this through convention rather than strict access control. A single leading underscore_namesignals "protected" - this is an internal implementation detail; do not touch it from outside unless you know what you are doing. A double underscore__nametriggers name mangling, where Python renames the attribute to_ClassName__name, making accidental access from outside much less likely.
Access Levels and @property
PythonHiding state while providing controlled access.
@classmethod and @staticmethod
A regular method receives self(the instance). A @classmethodreceives cls(the class itself) and is often used as an alternative constructor. A @staticmethodreceives neither; it is just a plain function that lives in the class's namespace because it is logically related to the class.
Three Method Types
PythonRegular, class, and static methods in one class.
Inheritance
Inheritance lets a new class (the child or subclass) take on all the attributes and methods of an existing class (the parent or superclass) and then add or change things as needed. Python supports four inheritance patterns.
Four Inheritance Patterns
Each pattern addresses a different structural need.
- Single: one child, one parent. The simplest and most common form.
- Multilevel: A inherits from B which inherits from C. A chain of specialisation.
- Multiple: one child inherits from two or more parents. Use carefully.
- Hierarchical: multiple children share a single parent (e.g., Cat and Dog both extend Animal).
Single and Multilevel Inheritance
PythonExtending and specialising behaviour through a chain.
super() and Method Resolution Order
super() returns a proxy object that delegates method calls to the next class in the MRO. It is most commonly used in __init__to ensure the parent's constructor runs before the child adds its own attributes. Always use super()rather than calling the parent class directly, especially in multiple inheritance, because it respects the MRO.
The Method Resolution Order is the sequence Python follows when looking up a method or attribute. It is computed using the C3 linearisation algorithm and always places the current class first, then its parents left to right, then their parents. You can inspect it withClassName.__mro__.
super() in Practice
PythonChaining constructors through the inheritance hierarchy.
Polymorphism and Duck Typing
Polymorphism means that different objects can respond to the same method call, each in their own way. In Python this is achieved through method overriding and, very commonly, through duck typing. The name comes from the saying: "if it walks like a duck and quacks like a duck, it is a duck." Python does not care about the class of an object; it only cares whether the object has the method or attribute being called. This makes Python functions naturally flexible.
Duck Typing in Action
PythonA function that works on anything with a speak() method.
Operator Overloading
Python operators like +,==, and<are implemented as dunder (double underscore) methods. By defining these methods on your class, you make your objects behave naturally with standard Python syntax, which makes code much cleaner than calling a named method likev1.add(v2).
Overloading + and ==
PythonMaking a custom Vector class work with standard operators.
Abstract Classes
An abstract class defines a contract: it declares methods that every subclassmust implement, but provides no implementation itself. You cannot instantiate an abstract class directly. This forces consistent interfaces across a family of related classes. Python'sabc module provides the tools: inherit fromABC and mark required methods with@abstractmethod.
Enforcing an Interface
PythonAbstract base class for a payment gateway.
__slots__ for Memory Optimization
By default, each Python object carries a__dict__ dictionary that holds all its attributes. For classes that create millions of instances (coordinates, data records, etc.) this overhead adds up. Defining__slots__tells Python exactly which attributes the class will ever have. Python then allocates fixed-size slots instead of a flexible dictionary, reducing memory usage by 40-50 percent in typical cases.
Using __slots__
PythonTrading flexibility for memory efficiency.
Composition vs Inheritance
Inheritance models an "is-a" relationship: aDog is anAnimal. Composition models a "has-a" relationship: aCar has anEngine. Experienced developers tend to prefer composition because it keeps classes loosely coupled. Changing the engine does not require touching the Car class; you just swap the component.
When to Choose Each
The relationship between concepts determines the right choice.
- Use inheritance when the child truly "is a" specialised version of the parent
- Use composition when the class "has" a behaviour provided by another object
- If you find yourself inheriting just to reuse a few methods, prefer composition
- Deep inheritance hierarchies (more than 2-3 levels) are a warning sign
Composition Example
PythonA Car that uses an Engine rather than inheriting from one.
Quiz - Test Your Knowledge
Eight questions covering the full breadth of OOP in Python. Some questions test conceptual understanding; others test specific syntax. Take your time and think through the implications of each answer.
Knowledge Check
1. What is the purpose of the __init__ method in a Python class?
2. What distinguishes a class variable from an instance variable?
3. Which name-mangling prefix makes an attribute "private" in Python?
4. What does super() return?
5. What is duck typing?
6. Which decorator turns a method into one that receives the class itself instead of an instance?
7. What is the purpose of __slots__?
8. In what situation is composition preferred over inheritance?