Object-Oriented Programming (OOP) in Python: A Hands-on A-Z Guide for New IT Professionals

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Quick Start: Get Started with Python OOP in 5 Minutes

Hey everyone, today let’s dive into Object-Oriented Programming (OOP) in Python. It might sound academic, but it’s actually a very effective way to organize code, making your projects much easier to manage and extend.

To get started quickly, we’ll create a simple NhanVien (Employee) class. A class is like a blueprint, while an object is an instance created from that blueprint.


class NhanVien:
    # Constructor method
    def __init__(self, ten, ma_so):
        self.ten = ten  # Name attribute
        self.ma_so = ma_so # ID attribute

    # Method to display information
    def hien_thi_thong_tin(self):
        print(f"Name: {self.ten}, ID: {self.ma_so}")

# Create objects from the NhanVien class
nhan_vien_1 = NhanVien("Nguyễn Văn A", "NV001")
nhan_vien_2 = NhanVien("Trần Thị B", "NV002")

# Call object methods
nhan_vien_1.hien_thi_thong_tin()
nhan_vien_2.hien_thi_thong_tin()

Run the code above, and you’ll see the output:


Name: Nguyễn Văn A, ID: NV001
Name: Trần Thị B, ID: NV002

See? With just a few lines of code, we’ve defined an “employee type” and created different “employee instances.” That’s the core idea of OOP!

Detailed Explanation: The Pillars of OOP in Python

After the quick start, let’s dive a bit deeper. OOP isn’t just about Classes and Objects. It has 4 main ‘pillars’ that help build robust code:

1. Encapsulation

Encapsulation means grouping related attributes (data) and methods (behavior) into a ‘bundle’ (class), and hiding the internal implementation details. This helps protect data from being accessed or modified externally in an unwanted way. In Python, we use naming conventions (leading underscores) to indicate which attributes should be ‘private,’ although Python doesn’t truly have private members like Java or C++.


class TaiKhoanNganHang:
    def __init__(self, ten_chu_tk, so_du_ban_dau):
        self.ten_chu_tk = ten_chu_tk
        self.__so_du = so_du_ban_dau # 'Private' convention using double underscores

    def nap_tien(self, so_tien):
        if so_tien > 0:
            self.__so_du += so_tien
            print(f"Successfully deposited {so_tien}. Current balance: {self.__so_du}")
        else:
            print("Deposit amount must be greater than 0.")

    def rut_tien(self, so_tien):
        if 0 < so_tien <= self.__so_du:
            self.__so_du -= so_tien
            print(f"Successfully withdrew {so_tien}. Current balance: {self.__so_du}")
        else:
            print("Invalid withdrawal amount or insufficient balance.")

    def xem_so_du(self):
        return self.__so_du

tk = TaiKhoanNganHang("Minh Hoang", 1000)
print(f"Initial balance: {tk.xem_so_du()}")
tk.nap_tien(500)
tk.rut_tien(200)
# Attempt direct access (not recommended)
# print(tk.__so_du) # Will raise AttributeError
# Instead, access via method
print(f"Final balance: {tk.xem_so_du()}")

2. Inheritance

Inheritance allows one class (child class or subclass) to inherit attributes and methods from another class (parent class or superclass). This helps in code reuse, avoiding the need to rewrite similar parts.


class HinhDang:
    def __init__(self, ten):
        self.ten = ten

    def mo_ta(self):
        return f"This is a shape named: {self.ten}"

class HinhTron(HinhDang):
    def __init__(self, ten, ban_kinh):
        super().__init__(ten) # Call the parent class's constructor
        self.ban_kinh = ban_kinh

    def dien_tich(self):
        return 3.14 * self.ban_kinh ** 2

class HinhVuong(HinhDang):
    def __init__(self, ten, canh):
        super().__init__(ten)
        self.canh = canh

    def dien_tich(self):
        return self.canh ** 2

hinh_tron = HinhTron("Vòng tròn lớn", 7)
hinh_vuong = HinhVuong("Ô vuông nhỏ", 5)

print(hinh_tron.mo_ta()) # Method from parent class
print(f"Area of the circle: {hinh_tron.dien_tich()}")
print(hinh_vuong.mo_ta())
print(f"Area of the square: {hinh_vuong.dien_tich()}")

3. Polymorphism

Polymorphism means that objects of different classes can respond differently to the same message (same method name). A typical example is method overriding from a parent class.


class ConVat:
    def noi(self):
        pass # Empty method, to be overridden

class Cho(ConVat):
    def noi(self):
        return "Woof woof!"

class Meo(ConVat):
    def noi(self):
        return "Meow meow!"

class Vit(ConVat):
    def noi(self):
        return "Quack quack!"

# A function that can handle different types of objects
def tieng_keu(con_vat):
    print(con_vat.noi())

cho = Cho()
meo = Meo()
vit = Vit()

tieng_keu(cho)
tieng_keu(meo)
tieng_keu(vit)

The output will be:


Woof woof!
Meow meow!
Quack quack!

Although the noi() method is called for all, each object exhibits different behavior.

4. Abstraction

Abstraction focuses on showing essential information and hiding complex details. In Python, we typically achieve abstraction through abstract classes and abstract methods by using the abc module.


from abc import ABC, abstractmethod

class PhuongTien(ABC):
    @abstractmethod
    def di_chuyen(self):
        pass

    @abstractmethod
    def dung_lai(self):
        pass

class XeOto(PhuongTien):
    def di_chuyen(self):
        print("The car is moving.")

    def dung_lai(self):
        print("The car has stopped.")

class XeMay(PhuongTien):
    def di_chuyen(self):
        print("The motorcycle is speeding recklessly.")

    def dung_lai(self):
        print("The motorcycle has stopped urgently.")

# p = PhuongTien() # Will raise an error because objects cannot be created from an Abstract Class

xemay = XeMay()
xemay.di_chuyen()
xemay.dung_lai()

xeto = XeOto()
xeto.di_chuyen()
xeto.dung_lai()

Advanced: Useful Techniques in Python OOP

Once the pillars are mastered, we can explore a few more ‘tricks’ to make code cleaner and more powerful.

1. The @property Decorator

The @property decorator allows you to access a method like an attribute, helping to control the reading/writing of attribute data without changing how it’s accessed externally.


class HocSinh:
    def __init__(self, ten, diem):
        self._ten = ten # 'Protected' convention
        self._diem = diem

    @property
    def diem(self):
        return self._diem

    @diem.setter
    def diem(self, new_diem):
        if 0 <= new_diem <= 10:
            self._diem = new_diem
        else:
            print("Score must be between 0 and 10.")

hs = HocSinh("Ngọc Anh", 8)
print(f"Score of {hs._ten}: {hs.diem}") # Access as attribute
hs.diem = 9.5 # Assign value as attribute
print(f"New score of {hs._ten}: {hs.diem}")
hs.diem = 12 # Will print a warning

2. Class Method and Static Method

  • Class Method (@classmethod): This method operates on the class itself, not on an instance of the class. It receives cls (instead of self) as its first argument, pointing to the class itself. Often used to create ‘alternative constructors’.
  • Static Method (@staticmethod): This method does not receive self or cls. It’s like a regular function but is placed within a class because it’s logically related to that class, without needing to access instance or class attributes or methods.

class HinhHoc:
    PI = 3.14159

    def __init__(self, ten):
        self.ten = ten

    @classmethod
    def tao_hinh_vuong_tu_chu_vi(cls, chu_vi):
        canh = chu_vi / 4
        return cls(f"Square with perimeter {chu_vi}", canh)

    @staticmethod
    def kiem_tra_so_duong(so):
        return so > 0

hv = HinhHoc.tao_hinh_vuong_tu_chu_vi(20)
print(f"Shape name: {hv.ten}") # hv.canh will raise an error because it only exists in HinhVuong if inherited and added as an attribute

print(f"Check if 5 is positive: {HinhHoc.kiem_tra_so_duong(5)}")
print(f"Check if -3 is positive: {HinhHoc.kiem_tra_so_duong(-3)}")

To make the above example code clearer for classmethods, you can imagine it will be more effective when inheritance is involved and an instance of the child class needs to be created from an ‘alternative constructor’.

Practical Tips: Applying OOP Effectively in Python

OOP is a powerful tool, but it’s important to know when and how to use it effectively.

1. When to use OOP?

  • When you have entities in the system with clear attributes and behaviors (e.g., Users, Products, Orders).
  • When you want to reuse code and easily extend functionality (inheritance, polymorphism).
  • When you need to manage the complexity of a large project, breaking it down into more logical modules.

2. Personal experience working with Python

In my experience, Python is very flexible. There are times when I need to handle strings or special patterns. When I need to quickly test regex patterns before integrating them into Python code, I often use a regex tester at toolcraft.app/en/tools/developer/regex-tester — it runs directly in the browser, no installation needed. This saves me a lot of time debugging small regex-related errors without having to rerun the entire Python script just to check a pattern.

3. Don’t overdo OOP!

Not every project requires complex OOP. For small, simple scripts that perform a few sequential tasks, applying OOP can make the code unnecessarily cumbersome. Choose the solution that fits the scale and requirements of the problem.

4. Clear code organization

Whether using OOP or functional programming, clear and readable naming for classes, methods, and variables is extremely important. I always strive to adhere to PEP 8, not just for myself but also for the team during code reviews.

Conclusion

Object-Oriented Programming in Python is a fundamental skill that helps you build large, well-structured, and maintainable applications. Through this article, I hope you have gained an overview and practiced the basic to advanced concepts of OOP. Keep practicing, read others’ code, and write a lot of code yourself, and it will ‘sink in’. Happy coding everyone!

Share: