Mastering OOP Basics in Python: Classes, Inheritance, and the Factory Pattern
Introduction
Ever stared at a codebase that feels like a tangled spaghetti of functions and wondered how to bring order? Object‑oriented programming (OOP) gives you a blueprint for grouping data and behavior into reusable, testable units. In this post we’ll expand on the one‑minute video that showed you the core of Python OOP, and we’ll walk through each concept with real‑world code, best‑practice tips, and a tiny design‑pattern example.
1. Defining a Class and __init__
The class keyword creates a new type. The special method __init__ (the constructor) runs every time you instantiate the class and is where you set up instance attributes.
class Person:
"""A simple data holder for a person's name and age."""
def __init__(self, name: str, age: int):
# Instance attributes are prefixed with `self`
self.name = name
self.age = age
Why self? It is the reference to the current object, allowing each instance to maintain its own state.
2. Adding Methods and Using self
Methods are functions that operate on an instance’s data. They always receive self as the first argument.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self) -> str:
"""Return a friendly greeting using the person's name."""
return f"Hello, my name is {self.name}."
def have_birthday(self) -> None:
"""Increment the person's age by one year."""
self.age += 1
Calling these methods:
p = Person("Ada", 30)
print(p.greet()) # → Hello, my name is Ada.
print(p.age) # → 30
p.have_birthday()
print(p.age) # → 31
3. Inheritance – Extending a Base Class
Inheritance lets a child class reuse and extend the behavior of a parent class. Use super() to delegate initialization to the base class.
class Employee(Person):
def __init__(self, name, age, salary):
# Initialise name and age via the parent
super().__init__(name, age)
self.salary = salary
def work(self) -> str:
return f"{self.name} is working for ${self.salary}/year."
emp = Employee("Grace", 28, 95000)
print(emp.greet()) # Inherited from Person
print(emp.work()) # Specific to Employee
You can also override methods to change behavior:
class Manager(Employee):
def work(self) -> str:
# Extend the parent implementation
base = super().work()
return base + " Managing the team now."
4. Simple Factory Pattern – Decoupling Object Creation
When client code needs to decide which subclass to instantiate, a Factory function centralises that logic. This keeps the creation code separate from the usage code and makes future extensions trivial.
class Developer(Employee):
def work(self) -> str:
return f"{self.name} is writing code."
class Manager(Employee):
def work(self) -> str:
return f"{self.name} is planning projects."
def employee_factory(role: str, *args, **kwargs) -> Employee:
"""Return an appropriate Employee subclass based on *role*.
Example:
dev = employee_factory('dev', 'Linus', 35, 120000)
"""
mapping = {
'dev': Developer,
'mgr': Manager,
}
try:
cls = mapping[role]
except KeyError as exc:
raise ValueError(f"Unknown role '{role}'. Expected one of {list(mapping)}") from exc
return cls(*args, **kwargs)
alice = employee_factory('dev', 'Alice', 29, 110000)
print(alice.work()) # → Alice is writing code.
bob = employee_factory('mgr', 'Bob', 40, 130000)
print(bob.work()) # → Bob is planning projects.
5. Best Practices, Common Pitfalls, and Performance Tips
| Area | Recommendation |
|---|---|
| Naming | Use CamelCase for class names, snake_case for methods/attributes. |
| Encapsulation | Prefix “private” attributes with a single underscore (_salary). Python does not enforce privacy, but the convention signals intent. |
Avoid Heavy Logic in __init__ |
Keep constructors cheap; perform expensive I/O or calculations outside the class or lazily via properties. |
| Prefer Composition Over Inheritance | When behaviour can be assembled from smaller objects, composition reduces tight coupling. |
| Factory Extensibility | Store the mapping in a registry dictionary that external modules can extend rather than hard‑coding it. |
| Testing | Unit‑test each class in isolation; mock dependencies when testing factories. |
| Performance | Instantiating many objects is cheap in CPython, but avoid unnecessary attribute creation inside tight loops. Use __slots__ if you need to squeeze memory for millions of objects. |
Pitfalls to Watch Out For
-
Forgotten
super()call – If you don’t callsuper().__init__, the base class’s attributes won’t be set, leading toAttributeErrors. -
Mutable default arguments – Never use a mutable object (e.g.,
list) as a default value in__init__; useNoneand initialise inside the method.
* Name clashes – Avoid using attribute names that shadow built‑in methods (self.id = ... is fine, but self.__dict__ is not).
Conclusion & Call‑to‑Action
You now have a solid foundation: define classes, attach methods, inherit responsibly, and employ a tiny Factory pattern to keep your codebase clean and extensible. Try building your own Vehicle hierarchy—Car, Truck, Motorcycle—and experiment with a factory that chooses the right subclass based on a configuration file. Share your patterns in the comments and keep the conversation rolling!
Happy coding!
Related Posts
Packaging Made Easy: A Step-by-Step Guide to Using Setuptools and Twine for PyPI
Simplify Python project packaging with setuptools and Twine for PyPI
Mastering Isolated Python Environments with Venv and Pip
Learn to create isolated Python environments with venv and manage packages with pip
Mastering Error Handling in Python with Try/Except Blocks
Learn to use try/except blocks to handle errors in Python
Mastering Python Functions and Collections
Learn reusable functions and collections in Python