Jun 20, 2026 · 4 min read

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 call super().__init__, the base class’s attributes won’t be set, leading to AttributeErrors.

  • Mutable default arguments – Never use a mutable object (e.g., list) as a default value in __init__; use None and 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

Jun 22, 2026

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

Jun 22, 2026

Mastering Isolated Python Environments with Venv and Pip

Learn to create isolated Python environments with venv and manage packages with pip

Jun 20, 2026

Mastering Error Handling in Python with Try/Except Blocks

Learn to use try/except blocks to handle errors in Python

Jun 19, 2026

Mastering Python Functions and Collections

Learn reusable functions and collections in Python