Object-Oriented Programming (OOP) allows you to model real-world concepts as objects with attributes (data) and methods (behavior). Python's OOP is clean and practical - you'll use it for everything from configuration objects to network device abstractions.
In this lab, you'll learn:
- Classes and objects
- The
selfconvention - Abstract base classes
- Inheritance and polymorphism
- Access modifiers (public, protected, private)
- Special methods (dunder methods like
__str__,__eq__) - Type checking with
isinstance()
The self parameter is Python's way of referring to the current instance of the class. It's not a keyword - just a convention (you could name it anything, but don't!).
class Dog:
"""A simple Dog class"""
def __init__(self, name, age):
"""
Constructor - called when creating a new Dog object
self = the instance being created
name, age = parameters we pass in
"""
self.name = name # Instance variable
self.age = age # Instance variable
def bark(self):
"""Instance method - self refers to THIS dog"""
print(f"{self.name} says: Woof!")
def get_info(self):
"""Access instance variables via self"""
return f"{self.name} is {self.age} years old"
# Create objects (instances)
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
# Call methods - Python automatically passes 'self'
dog1.bark() # Python calls: Dog.bark(dog1)
dog2.bark()
print(dog1.get_info())
print(dog2.get_info())
# Access attributes directly
print(f"\nDirect access: {dog1.name}, {dog2.name}")Output:
Buddy says: Woof!
Max says: Woof!
Buddy is 3 years old
Max is 5 years old
Direct access: Buddy, Max
Key concept: self is automatically passed as the first parameter. When you call dog1.bark(), Python translates it to Dog.bark(dog1).
Abstract base classes (ABCs) define a contract - methods that subclasses MUST implement.
from abc import ABC, abstractmethod
class Animal(ABC):
"""
Abstract base class - cannot be instantiated directly
Defines the contract for all animals
"""
def __init__(self, name, age):
"""Constructor - called by subclasses"""
self.name = name
self.age = age
self._species = "Unknown" # Protected attribute (by convention)
@abstractmethod
def make_sound(self):
"""
Abstract method - MUST be implemented by subclasses
This method has no body here
"""
pass
@abstractmethod
def move(self):
"""Another abstract method"""
pass
def sleep(self):
"""
Concrete method - inherited by all subclasses
Already has implementation
"""
print(f"{self.name} is sleeping... Zzz")
def get_info(self):
"""Concrete method using instance variables"""
return f"{self.name} ({self._species}) is {self.age} years old"
# Try to create an Animal - this will FAIL!
try:
animal = Animal("Generic", 5)
except TypeError as e:
print(f"Cannot instantiate abstract class: {e}")Output:
Cannot instantiate abstract class: Can't instantiate abstract class Animal with abstract methods make_sound, move
Why abstract classes? They enforce a contract. Every Animal MUST implement make_sound() and move(). Other functions or systems will know that if you truly are an "Animal" then they can expect you to have make_sound() and move()
from abc import ABC, abstractmethod
class Animal(ABC):
"""Abstract base class"""
def __init__(self, name, age):
self.name = name
self.age = age
self._species = "Unknown"
@abstractmethod
def make_sound(self):
pass
@abstractmethod
def move(self):
pass
def sleep(self):
print(f"{self.name} is sleeping... Zzz")
def get_info(self):
return f"{self.name} ({self._species}) is {self.age} years old"
class Cat(Animal):
"""
Cat inherits from Animal
MUST implement abstract methods
"""
def __init__(self, name, age, indoor=True):
# Call parent constructor
super().__init__(name, age)
self._species = "Feline" # Set protected attribute
self.__indoor = indoor # Private attribute (name mangling)
def make_sound(self):
"""Implement abstract method"""
print(f"{self.name} says: Meow!")
def move(self):
"""Implement abstract method"""
print(f"{self.name} gracefully walks on silent paws")
def purr(self):
"""Cat-specific method (not in Animal)"""
print(f"{self.name} is purring... purrrr")
def is_indoor(self):
"""Access private attribute via method"""
return self.__indoor
# Create a Cat
cat = Cat("Whiskers", 3)
# Call inherited method
cat.sleep()
# Call implemented abstract methods
cat.make_sound()
cat.move()
# Call Cat-specific method
cat.purr()
# Call inherited get_info
print(cat.get_info())
# Check indoor status
print(f"Indoor cat: {cat.is_indoor()}")Output:
Whiskers is sleeping... Zzz
Whiskers says: Meow!
Whiskers gracefully walks on silent paws
Whiskers is purring... purrrr
Whiskers (Feline) is 3 years old
Indoor cat: True
Python uses naming conventions for access control (not enforced by the language like Java/C++).
class AccessDemo:
"""Demonstrates access modifier conventions"""
def __init__(self):
# Public - accessible from anywhere
self.public_var = "I'm public!"
# Protected - by convention, don't access outside class/subclasses
# Single underscore prefix
self._protected_var = "I'm protected (by convention)"
# Private - name mangling makes it harder to access
# Double underscore prefix
self.__private_var = "I'm private!"
def public_method(self):
"""Public method - anyone can call"""
return "Public method called"
def _protected_method(self):
"""Protected method - by convention, internal use"""
return "Protected method called"
def __private_method(self):
"""Private method - name mangled"""
return "Private method called"
def demonstrate_access(self):
"""Show we can access everything internally"""
print(f"Public: {self.public_var}")
print(f"Protected: {self._protected_var}")
print(f"Private: {self.__private_var}")
print(f"Private method: {self.__private_method()}")
obj = AccessDemo()
# Public - works fine
print(obj.public_var)
print(obj.public_method())
print()
# Protected - works, but you shouldn't (by convention)
print(obj._protected_var)
print(obj._protected_method())
print()
# Private - name mangled, hard to access
try:
print(obj.__private_var)
except AttributeError as e:
print(f"Can't access private: {e}")
# Python mangles the name to _ClassName__attribute
# You CAN still access it, but it's discouraged
print(f"Accessing mangled name: {obj._AccessDemo__private_var}")
print()
# Internal access works
obj.demonstrate_access()Output:
I'm public!
Public method called
I'm protected (by convention)
Protected method called
Can't access private: 'AccessDemo' object has no attribute '__private_var'
Accessing mangled name: I'm private!
Public: I'm public!
Protected: I'm protected (by convention)
Private: I'm private!
Private method: Private method called
Key points:
public_var- anyone can access_protected_var- convention: internal use only__private_var- name mangled to_ClassName__private_var- Python doesn't enforce privacy - it's based on trust
Polymorphism means "many forms" - different classes can be treated the same way if they share a common interface.
from abc import ABC, abstractmethod
class Animal(ABC):
"""Abstract base class"""
def __init__(self, name, age):
self.name = name
self.age = age
self._species = "Unknown"
@abstractmethod
def make_sound(self):
"""Every animal must make a sound"""
pass
@abstractmethod
def move(self):
"""Every animal must be able to move"""
pass
def sleep(self):
"""Common behavior - inherited by all"""
print(f"{self.name} is sleeping... Zzz")
def get_info(self):
return f"{self.name} ({self._species}) is {self.age} years old"
class Cat(Animal):
"""Cat implementation"""
def __init__(self, name, age, indoor=True):
super().__init__(name, age)
self._species = "Feline"
self.__indoor = indoor
def make_sound(self):
print(f"{self.name} says: Meow!")
def move(self):
print(f"{self.name} gracefully walks")
def purr(self):
print(f"{self.name} is purring...")
class Dog(Animal):
"""Dog implementation"""
def __init__(self, name, age, breed):
super().__init__(name, age)
self._species = "Canine"
self.__breed = breed
def make_sound(self):
print(f"{self.name} says: Woof!")
def move(self):
print(f"{self.name} runs energetically")
def fetch(self):
print(f"{self.name} fetches the ball!")
# Polymorphic function - works with ANY Animal
def animal_action(animal):
"""
Takes any Animal and calls its methods
Polymorphism - same interface, different behavior
"""
# Type checking
if not isinstance(animal, Animal):
print(f"Error: {animal} is not an Animal!")
return
print(f"\n--- {animal.name} ---")
print(animal.get_info())
animal.make_sound() # Polymorphic call - behavior depends on type
animal.move() # Polymorphic call
animal.sleep() # Inherited method
# Create animals
cat = Cat("Whiskers", 3)
dog = Dog("Buddy", 5, "Golden Retriever")
# Polymorphism - same function, different behavior
animal_action(cat)
animal_action(dog)
# Type checking with isinstance
print(f"\nIs cat an Animal? {isinstance(cat, Animal)}")
print(f"Is cat a Cat? {isinstance(cat, Cat)}")
print(f"Is cat a Dog? {isinstance(cat, Dog)}")
# Try with a non-Animal
animal_action("Not an animal")Output:
--- Whiskers ---
Whiskers (Feline) is 3 years old
Whiskers says: Meow!
Whiskers gracefully walks
Whiskers is sleeping... Zzz
--- Buddy ---
Buddy (Canine) is 5 years old
Buddy says: Woof!
Buddy runs energetically
Buddy is sleeping... Zzz
Is cat an Animal? True
Is cat a Cat? True
Is cat a Dog? False
Error: Not an animal is not an Animal!
Polymorphism in action: The animal_action() function works with ANY Animal - it doesn't care if it's a Cat or Dog. The correct make_sound() is called automatically.
Special methods (surrounded by double underscores) let you define how objects behave with operators and built-in functions.
class Animal:
"""Simple Animal without dunder methods"""
def __init__(self, name, age):
self.name = name
self.age = age
# Without __str__
animal = Animal("Buddy", 5)
print(animal) # Ugly output!
print()
class BetterAnimal:
"""Animal with __str__ and __repr__"""
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
"""
Called by str() and print()
Should be readable for end users
"""
return f"{self.name} (age {self.age})"
def __repr__(self):
"""
Called by repr() and in REPL
Should be unambiguous, ideally valid Python code
"""
return f"BetterAnimal('{self.name}', {self.age})"
# With __str__ and __repr__
better = BetterAnimal("Max", 3)
print(better) # Calls __str__
print(repr(better)) # Calls __repr__
print(f"Animal: {better}") # Calls __str__
# In a list, __repr__ is used
animals = [better, BetterAnimal("Bella", 7)]
print(animals)Output:
<__main__.Animal object at 0x7f8b3c4d5e10>
Max (age 3)
BetterAnimal('Max', 3)
Animal: Max (age 3)
[BetterAnimal('Max', 3), BetterAnimal('Bella', 7)]
class Animal:
"""Animal without __eq__"""
def __init__(self, name, age):
self.name = name
self.age = age
# Without __eq__ - compares memory addresses
animal1 = Animal("Buddy", 5)
animal2 = Animal("Buddy", 5)
print(f"animal1 == animal2: {animal1 == animal2}") # False - different objects
print(f"animal1 is animal2: {animal1 is animal2}") # False
print()
class BetterAnimal:
"""Animal with __eq__"""
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
"""
Define what equality means
Two animals are equal if name and age match
"""
if not isinstance(other, BetterAnimal):
return False
return self.name == other.name and self.age == other.age
def __str__(self):
return f"{self.name} (age {self.age})"
# With __eq__ - compares attributes
better1 = BetterAnimal("Max", 3)
better2 = BetterAnimal("Max", 3)
better3 = BetterAnimal("Max", 5)
print(f"better1 == better2: {better1 == better2}") # True - same name/age
print(f"better1 == better3: {better1 == better3}") # False - different age
print(f"better1 is better2: {better1 is better2}") # False - different objectsOutput:
animal1 == animal2: False
animal1 is animal2: False
better1 == better2: True
better1 == better3: False
better1 is better2: False
class Animal:
"""Animal with multiple dunder methods"""
def __init__(self, name, age, weight):
self.name = name
self.age = age
self.weight = weight
def __str__(self):
return f"{self.name} (age {self.age}, {self.weight}kg)"
def __repr__(self):
return f"Animal('{self.name}', {self.age}, {self.weight})"
def __eq__(self, other):
"""Equality - same name, age, weight"""
if not isinstance(other, Animal):
return False
return (self.name == other.name and
self.age == other.age and
self.weight == other.weight)
def __lt__(self, other):
"""Less than - compare by weight"""
if not isinstance(other, Animal):
return NotImplemented
return self.weight < other.weight
def __len__(self):
"""Length - return age in months"""
return self.age * 12
def __bool__(self):
"""Truthiness - False if age is 0"""
return self.age > 0
def __add__(self, other):
"""Addition - combine weights (silly example)"""
if not isinstance(other, Animal):
return NotImplemented
return self.weight + other.weight
# Create animals
cat = Animal("Whiskers", 3, 4.5)
dog = Animal("Buddy", 5, 25.0)
puppy = Animal("Newborn", 0, 2.0)
# __str__ and __repr__
print(cat)
print(repr(dog))
print()
# __eq__
cat2 = Animal("Whiskers", 3, 4.5)
print(f"cat == cat2: {cat == cat2}")
print(f"cat == dog: {cat == dog}")
print()
# __lt__ (enables sorting)
print(f"cat < dog: {cat < dog}") # Compare by weight
animals = [dog, cat, puppy]
animals.sort() # Uses __lt__
print(f"Sorted by weight: {animals}")
print()
# __len__
print(f"Cat age in months: {len(cat)}")
print(f"Dog age in months: {len(dog)}")
print()
# __bool__
if cat:
print(f"{cat.name} is active (age > 0)")
if not puppy:
print(f"{puppy.name} is newborn (age == 0)")
print()
# __add__
total_weight = cat + dog
print(f"Combined weight: {total_weight}kg")Output:
Whiskers (age 3, 4.5kg)
Animal('Buddy', 5, 25.0)
cat == cat2: True
cat == dog: False
cat < dog: True
Sorted by weight: [Animal('Newborn', 0, 2.0), Animal('Whiskers', 3, 4.5), Animal('Buddy', 5, 25.0)]
Cat age in months: 36
Dog age in months: 60
Whiskers is active (age > 0)
Newborn is newborn (age == 0)
Combined weight: 29.5kg
Your task: Create a network device management system using OOP
Note: You don't need real network data or working connections. This is just for practicing OOP concepts. Create the classes with the right structure, and you can provide simple data (like "Router1", "10.0.0.1") when you create objects. The focus is on the class design, not real network functionality.
Requirements:
-
Create an abstract
NetworkDeviceclass with:- Constructor:
hostname,ip_address,uptime_hours - Abstract methods:
connect(),backup_config() - Concrete method:
reboot()(prints message) - Protected attribute:
_connection_status - Private attribute:
__last_backup_time
- Constructor:
-
Create two subclasses:
Router- implements abstract methods, hasrouting_protocolattributeSwitch- implements abstract methods, hasvlan_countattribute
-
Implement dunder methods:
__str__- user-friendly representation__repr__- technical representation__eq__- devices are equal if same hostname and IP__lt__- compare by uptime for sorting
-
Create a polymorphic function:
backup_all_devices(devices)- takes a list of devices- Checks if each is a
NetworkDevicewithisinstance() - Calls
backup_config()on each (polymorphism) - Returns count of successful backups
-
Test with:
- Create 2 routers and 2 switches
- Add to a list
- Call
backup_all_devices() - Sort devices by uptime
- Print the sorted list
Bonus: Add a get_device_info() method that returns a dictionary with all device info.
You've completed this lab when you can:
- Create classes with constructors (
__init__) - Understand and use the
selfkeyword - Create abstract base classes with
ABCand@abstractmethod - Implement inheritance with
super() - Use access modifiers (public,
_protected,__private) - Implement polymorphic functions using
isinstance() - Implement dunder methods (
__str__,__repr__,__eq__,__lt__) - Explain how polymorphism works with inheritance
What you learned:
- Classes and objects - Blueprint (class) vs instance (object)
self- Reference to the current instance- Abstract base classes - Define contracts with
@abstractmethod - Inheritance - Subclasses extend parent classes
- Access modifiers - Conventions: public,
_protected,__private - Polymorphism - Same interface, different implementations
- Dunder methods - Customize object behavior (
__str__,__eq__, etc.) - Type checking - Use
isinstance()to check object types
Why this matters:
- Model network devices, servers, configurations as objects
- Create reusable, maintainable code
- Essential for working with APIs (objects everywhere!)
- Foundation for frameworks like Django, Flask, FastAPI
- Common in automation tools (Ansible modules, network SDKs)
Next steps: Apply OOP to real automation scripts - network device management, configuration objects, API clients.
# Representation
__str__(self) # Called by str() and print()
__repr__(self) # Called by repr(), should be unambiguous
# Comparison
__eq__(self, other) # ==
__ne__(self, other) # !=
__lt__(self, other) # <
__le__(self, other) # <=
__gt__(self, other) # >
__ge__(self, other) # >=
# Numeric operations
__add__(self, other) # +
__sub__(self, other) # -
__mul__(self, other) # *
__truediv__(self, other) # /
# Container behavior
__len__(self) # len()
__getitem__(self, key) # obj[key]
__setitem__(self, key, value) # obj[key] = value
__contains__(self, item) # item in obj
# Other
__bool__(self) # bool(), truthiness
__call__(self) # Make object callable like a function
__hash__(self) # hash(), needed for set/dict keysCongratulations! You now understand Python OOP fundamentals - the foundation for building scalable automation tools and working with modern Python frameworks!