Object-Oriented Programming in Python
Learning objectives
In this in-depth guide you will learn to:
- distinguish between a Class (the blueprint) and an Object (the instance);
- use the
__init__constructor and theselfparameter; - define methods to encapsulate logic;
- use “Magic Methods” such as
__str__; - apply OOP to create an
IPAddressclass for networking.
Core Concepts
Object-Oriented Programming (OOP) lets us structure code by grouping data and behaviour into a single container.
- Class: The “blueprint” or template. It defines what attributes (data) and methods (actions) the object will have. Example: the blueprint of an IP address.
- Object (Instance): A concrete object created from the class. Example: the IP
192.168.1.10. - Attributes: Variables that hold the object’s data (e.g. octets, mask).
- Methods: Functions that operate on the object’s data (e.g. convert to binary).
Basic Syntax in Python
The __init__ Constructor
A special method called automatically when a new object is created. It initialises the attributes.
The self parameter
Represents the current instance of the object. When you call a method on an object, Python automatically passes the object itself as the first argument — that is why it must always be declared.
class Dog:
# Constructor
def __init__(self, name, breed):
self.name = name # Instance attribute
self.breed = breed # Instance attribute
# Method
def bark(self):
return f"{self.name} says: Woof!"
# Creating objects (Instantiation)
fido = Dog("Fido", "Labrador")
rex = Dog("Rex", "German Shepherd")
print(fido.bark()) # Fido says: Woof!
Encapsulation and Getters/Setters
In Python, encapsulation is not as strict as in other languages (e.g. Java or C++), but it is good practice not to access internal attributes directly when you want to control how they are read or modified.
Why use them?
Imagine you have an age attribute. If you let anyone do person.age = -5, you end up with invalid data. Using a setter, you can validate the value before assigning it.
The Pythonic solution: @property
Instead of writing get_age() and set_age() methods, Python uses decorators to make methods look like ordinary attributes.
class Student:
def __init__(self, name, grade):
self.name = name
self._grade = grade # _ signals "internal use" (convention)
@property
def grade(self):
"""Getter: called when you write student.grade"""
return self._grade
@grade.setter
def grade(self, new_grade):
"""Setter: called when you write student.grade = 10"""
if 0 <= new_grade <= 10:
self._grade = new_grade
else:
print("Invalid grade! Must be between 0 and 10.")
s = Student("Mario", 6)
print(s.grade) # Calls the getter -> 6
s.grade = 12 # Calls the setter -> Invalid grade!
s.grade = 8 # Calls the setter -> OK
Static Methods
Sometimes you need a function that logically belongs to a class but does not need access to self (i.e. to the data of a specific instance). These are static methods.
They are defined with the @staticmethod decorator and do not take self as their first argument.
class Maths:
@staticmethod
def add(a, b):
return a + b
# No need to create an object!
print(Maths.add(5, 3)) # 8
Dataclasses
We often create classes just to hold data, writing a long __init__ and a repetitive __str__. Since Python 3.7, dataclasses automate all of this.
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int = 1 # Default value
# __init__ and __str__ are generated automatically!
p = Product("Laptop", 999.99)
print(p)
# Output: Product(name='Laptop', price=999.99, quantity=1)
Case Study: The IPAddress Class
Instead of handling IP addresses and netmasks as plain strings scattered throughout the code, we create a class that encapsulates the networking logic.
Class Structure
We want to create an IP, display it, and (in the future) perform subnetting calculations.
class IPAddress:
def __init__(self, ip_string, cidr):
"""
Initialises the object.
:param ip_string: e.g. "192.168.1.10"
:param cidr: integer for the mask, e.g. 24 (for /24)
"""
self.ip_string = ip_string
self.cidr = cidr
# We can calculate the netmask immediately or later
self.netmask_string = self._cidr_to_netmask(cidr)
def __str__(self):
"""
Magic method for string representation.
Called when you do print(object).
"""
return f"IP: {self.ip_string}/{self.cidr} (Mask: {self.netmask_string})"
def _cidr_to_netmask(self, cidr):
"""
'Private' method (convention: _) to convert /24 to 255.255.255.0.
"""
# Simplified example (for demonstration only):
if cidr == 24: return "255.255.255.0"
if cidr == 16: return "255.255.0.0"
if cidr == 8: return "255.0.0.0"
return "Unknown"
def to_binary(self):
"""Returns the IP address in binary format (32-bit string)."""
# EXERCISE: Implement the conversion
# Hint: split('.'), int(), bin(), zfill(8)
pass
def get_network_address(self):
"""Returns the network address."""
# EXERCISE: Apply bitwise AND between IP and Mask
pass
def is_same_network(self, other_ip_object):
"""
Checks whether this IP and another are on the same subnet.
:param other_ip_object: Another IPAddress instance
"""
# EXERCISE: Compare the network addresses
pass
Using the Class
Here is how we will use our class once it is complete:
# Creating two IP objects
ip1 = IPAddress("192.168.1.50", 24)
ip2 = IPAddress("192.168.1.200", 24)
ip3 = IPAddress("192.168.2.10", 24)
print(ip1)
# Expected output: IP: 192.168.1.50/24 (Mask: 255.255.255.0)
# Network check (once you implement the methods)
# if ip1.is_same_network(ip2):
# print("They are on the same local network!")
Practical Exercise
Goal: Complete the IPAddress class
Now it is your turn to implement the missing logic.
- Binary Conversion: Complete
to_binary(). The address192.168.1.1must become a 32-bit string (or a list of 4 bytes). - Mask Handling: Improve
_cidr_to_netmaskto handle any CIDR value (from /0 to /32), not just the standard ones. - Network Address: Implement
get_network_address(). Remember to perform the bitwise AND between the IP and the Netmask. - Network Verification: Complete
is_same_network(self, other). It should returnTrueif the two IPs have the same network address,Falseotherwise. - Bonus — Broadcast: Add a method to calculate the Broadcast address of the network.
Hint: To work with bits in Python, remember the bitwise operators: & (AND), | (OR), ~ (NOT). It is often convenient to convert the entire IP into a single 32-bit integer for calculations.
EC