super()A class is a blueprint for creating objects.
Basic syntax:
class SuperHero:
def __init__(self, name: str, power: str, health: int, speed: int):
self.name = name
self.power = power
self.health = health
self.speed = speed
__init__ is a special method that creates an object and initializes its attributes. It sets up the initial state of the object.
Name convention: each word in the class name starts with a capital letter (no underscores).
An object is an instance of a class.
# Creating superhero objects
iron_man = SuperHero("Iron Man", "repulsor beams", 100, 80)
spider_man = SuperHero("Spider Man", "web slinging", 90, 95)
Object attributes are properties that belong to the object.
Object methods are functions that belong to a class/object. Methods are functions that are defined inside a class. A function defined outside a class is not a method.
Docstrings describe functions, methods, and classes and serve as documentation for your code.
class SuperHero:
def __init__(self, name: str, power_level: int, public_name: str):
self._name = name # protected attribute
self._power_level = power_level # protected attribute
self.public_name = public_name # public attribute
def get_name(self) -> str: # public method
return self._name
def _some_protected_method(self) -> None: # protected method
pass
Child classes can access protected attributes/methods.
class SuperHero:
def __init__(self, name: str, power_level: int):
self.__name = name # private attribute
self.__power_level = power_level # private attribute
# private method
def __secret_power(self) -> str:
return f"Using {self.__name}'s secret power!"
Private is a stronger form of encapsulation and can’t be accessed via a child class. It can only be accessed within the class.
A getter is a method that returns a private/protected attribute. A setter is a method that sets the value of a private/protected attribute.
The idiomatic way to use getter and setter methods is with the @property and @<name>.setter decorators.
class Hero:
def __init__(self, name: str):
self.__name = name # private attribute
# Getter
@property
def name(self) -> str:
return self.__name
# Setter
@name.setter
def name(self, new_name: str) -> None:
if new_name != "":
self.__name = new_name
else:
print("Name cannot be empty!")
hero = Hero("Batman")
# Getting name
print(hero.name) # this calls the getter method, not the attribute
# Setting name
hero.name = "Superman" # this calls the setter method, not the attribute
hero.name = "" # Error: Name cannot be empty!
@property and the corresponding setter make code look cleaner and feel more natural to use.
class Superhero:
hero_count = 0 # Class attribute
def __init__(self, name: str, power: str):
self.name = name # Instance attribute
self.power = power # Instance attribute
Superhero.hero_count += 1
class Superhero:
training_level = 1 # Class attribute
def __init__(self, name: str, power: str):
self.name = name # Instance attribute
self.power = power # Instance attribute
@classmethod
def upgrade_training(cls) -> None:
cls.training_level += 1
print(f"All heroes now at training level {cls.training_level}")
Superhero.upgrade_training() # Recommended way to use class method
print(Superhero.training_level) # 2
Class methods don’t have access to instance attributes.
Class methods can be defined with additional parameters after the cls parameter.
class Superhero:
def __init__(self, name: str, power: str):
if not self.is_valid_power(power):
raise ValueError(f"Invalid power: {power}")
self.name = name # Instance attribute
self.power = power # Instance attribute
@staticmethod
def is_valid_power(power: str) -> bool:
valid_powers = ["Flying", "Strength", "Speed", "Intelligence"]
for valid_power in valid_powers: # Iterate over each valid power and check if the power matches
if power == valid_power:
return True
return False
Static methods are similar to class methods but:
self or clsInheritance allows us to create a new class based on an existing class. The new class is known as a child class/subclass.
class Superhero:
def __init__(self, name: str, power: str):
self.name = name
self.power = power
class Avenger(Superhero):
def fly(self) -> None:
print(f"{self.name} can fly using {self.power}")
iron_man = Avenger("Iron Man", "repulsor beams")
iron_man.fly() # Iron Man can fly using repulsor beams
class Superhero:
def __init__(self, name: str):
self.name = name
def fight(self) -> None:
print("Superhero fights with advanced weapons!")
class Avenger(Superhero):
# Override the fight method
def fight(self) -> None:
print("Avenger fights with advanced weapons!")
avenger = Avenger("Iron Man")
avenger.fight() # Output: Avenger fights with advanced weapons!
super()super() extends parent class behavior. We can access parent class methods and properties:
class ParentClass:
def parent_method(self) -> None:
print("This is the parent class method")
class ChildClass(ParentClass):
def __init__(self) -> None:
super().__init__() # Call parent class __init__ (if defined)
def child_method(self) -> None:
super().parent_method() # Call parent class's instance method
print("This is the child class method")
Multiple inheritance means a class can inherit from more than one parent class.
class Swimmer:
def swim(self):
print("Swimming")
class Flyer:
def fly(self):
print("Flying")
# Duck inherits from both Swimmer and Flyer
class Duck(Swimmer, Flyer):
pass
duck = Duck()
duck.swim() # Should print "Swimming"
duck.fly() # Should print "Flying"
Polymorphism means "many forms" — the same interface, different implementations.
class Superhero:
def __init__(self, name: str, power: str):
self.name = name
self.power = power
def special_power(self) -> None:
pass # Not needed for this example
class IronMan(Superhero):
def special_power(self) -> None:
print(f"{self.name} uses {self.power}")
class Thor(Superhero):
def special_power(self) -> None:
print(f"{self.name} uses {self.power}")
def display_power(hero: Superhero) -> None:
hero.special_power()
iron_man = IronMan("Iron Man", "repulsor beams")
thor = Thor("Thor", "hammer")
display_power(iron_man) # Iron Man uses repulsor beams
display_power(thor) # Thor uses hammer
Method overloading is having multiple behaviors for a method name, based on arguments. In Python, this is usually done with default arguments or variable-length arguments.
class Calculator:
# Method 1: Default arguments
def add(self, a: int, b: int, c: int = 0) -> int:
return a + b + c
# Method 2: Variable-length arguments
def add_multiple(self, *args: int) -> int:
return sum(args)
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self) -> bool:
pass
@abstractmethod
def query(self, sql: str) -> list:
pass
Abstraction forces subclasses to implement abstract methods. If not implemented, it will throw an error when you try to instantiate the subclass.
| Concept | Key Points | Use When |
|---|---|---|
| Classes | Blueprint for objects, use PascalCase | Creating reusable object templates |
| Encapsulation | public, _protected, __private |
Controlling access to data |
| Properties | @property and @setter decorators |
Need validation or computed attributes |
| Inheritance | class Child(Parent): |
Extending existing functionality |
| Polymorphism | Same interface, different implementations | Code that works with multiple types |
| Abstraction | @abstractmethod from ABC |
Enforcing implementation contracts |
💡 Pro Tip: Start with simple classes and gradually add complexity. Focus on clear, descriptive names and keep methods focused on a single responsibility.