Class vs. Instance
Imagine a car factory.
- Class: This is the blueprint. It describes what a car is (it has wheels, a color, and an engine) but it isn’t a physical car yet.
- Object / Instance: This is the actual car that rolls off the assembly line. Your red Toyota and your neighbor’s blue Ford are different instances of the “Car” blueprint.
class Car:
pass
car_1 = Car() # Instance 1
car_2 = Car() # Instance 2
print(car_1) # Shows a unique memory address
People often say “Car Object”. What they mean is “a specific instance created from the Car class”. In this case, the words “object” and “instance” are interchangeable in meaning.
__init__ and self
When you build a car, you need to set its color and brand immediately. We use the __init__ method for this.
class Car:
def __init__(self, brand, model, price):
# Instance Variables
self.brand = brand
self.model = model
self.price = price
# Instance Method
def description(self):
return f"This is a {self.brand} {self.model} costing ${self.price}"
my_car = Car("Tesla", "Model 3", 45000)
print(my_car.description())
What is a method?
A method is simply a function that belongs to a class. While a regular function stands on its own, a method is tied to a specific instance (like your actual car_1) and usually performs an action using that car’s unique data.
- Function:
drive()(Generic) - Method:
my_car.drive()(Specifically telling your car to move.)
What are those __something__?
They are called Magic Methods or Dunder Methods (“Dunder” stands for Double Underscore). These are built-in “superpowers” that let you tell Python how to handle your object (instance) in specific situations.
Think of them as Automatic Triggers. You don’t usually call my_car.__str__() manually, Python triggers it automatically when you try to print(my_car).
__init__: Triggers when you create the car. Sets up the attributes.__str__: Triggers when youprint(). It should be a pretty, readable string for users.__repr__: Triggers during debugging withrepr(). It should be an unambiguous string that shows exactly how to recreate the object.__add__: Triggers when you use the+operator.
class Car:
def __init__(self, brand, model, price):
self.brand = brand
self.model = model
self.price = price
def __repr__(self):
# Goal: Look like the code used to create the car
return f"Car('{self.brand}', '{self.model}', {self.price})"
def __str__(self):
return f"{self.brand} {self.model} (${self.price})"
def __add__(self, other):
# We can define what happens when we "add" two cars
return self.price + other.price
car1 = Car("Toyota", "Camry", 20000)
car2 = Car("Honda", "Civic", 18000)
print(repr(car1)) # Output: Car('Toyota', 'Camry', 20000)
print(car1) # Output: Toyota Camry ($20000)
print(car1 + car2) # Output: 38000
What is self?
self represents the specific instance (specific car) that the code is currently working on.
Imagine 100 cars are rolling off the assembly line. When a mechanic goes to “paint” a car red, they need to know which car to paint. self is like the mechanic pointing at a specific car and saying, “I’m working on this one.”
Under the Hood
When you call a method like my_car.description(), Python actually transforms it into a class-level call behind the scenes. It automatically passes your object as the first argument.
my_car = Car("Tesla", "Model 3", 45000)
# How you write it:
my_car.description()
# What Python does "Under the Hood":
# It calls the class, and passes the specific instance (my_car) into 'self'
Car.description(my_car)
Without it, the description() method wouldn’t know which car’s price or brand to look up
Class vs. Instance Variables
- Instance Variables: Unique to each car (e.g., color, brand).
- Class Variables: Shared by all cars from that blueprint (e.g.,
number_of_wheels = 4).
class Car:
num_of_wheels = 4 # Class variable: Every car has 4 wheels
total_cars_made = 0
def __init__(self, brand):
self.brand = brand # Instance variable
Car.total_cars_made += 1 # We use 'Car' because this is shared data
c1 = Car("Audi")
c2 = Car("BMW")
print(Car.total_cars_made) # 2
Method Types: Regular, Class, and Static
- Regular Methods (self): Use these when you need to change or see data about a specific car.
- Class Methods (
@classmethod): Use these to change data for the whole factory (the blueprint). They takeclsinstead ofself. - Static Methods (
@staticmethod): Use these for simple functions that relate to cars but don’t need to know anything about a specific car (we don’t useselfthere).
class Car:
tax_rate = 1.05 # Class Variable
def __init__(self, brand, price):
self.brand = brand # Instance variable
self.price = price
# Regular Method: Changes ONE car
def apply_discount(self):
self.price = self.price * 0.9
# Class Method: Changes a FACTORY-WIDE (class rule like the tax rate)
# This affects every single car made by this blueprint.
@classmethod
def change_tax(cls, new_rate):
cls.tax_rate = new_rate
# Static Method: Just a helpful tool
# We don't need or use 'self' here.
@staticmethod
def is_safe_speed(speed):
return speed < 120
# Create two specific cars (Instances)
car_1 = Car("Toyota", 20000)
car_2 = Car("BMW", 40000)
# --- CALLING REGULAR METHODS ---
# This only affects car_1. car_2's price stays the same.
car_1.apply_discount()
print(car_1.price) # 18000.0
print(car_2.price) # 40000 (No change)
# --- CALLING CLASS METHODS ---
# We call this on the CLASS itself (the factory).
Car.change_tax(1.10)
# Now EVERY car sees the new tax rate
print(car_1.tax_rate) # 1.10
print(car_2.tax_rate) # 1.10
# --- CALLING STATIC METHODS ---
# We usually call this on the CLASS. It's just a utility tool.
# It doesn't need to know about car_1 or car_2 to work.
is_safe = Car.is_safe_speed(100)
print(is_safe) # True
What is cls?
Just like self is a placeholder for the Instance (the specific car), cls is a placeholder for the Class (the blueprint itself).
- When you use
self, you are saying: “Change this specific car.” - When you use
cls, you are saying: “Change the blueprint for every car.”
The most common real-world use for cls is creating an “Alternative Constructor”. Imagine you have car data coming in as a string like "Toyota-Camry-20000". Instead of splitting that string manually every time, you can use a class method to handle it.
class Car:
def __init__(self, brand, model, price):
self.brand = brand
self.model = model
self.price = price
@classmethod
def from_string(cls, car_string):
brand, model, price = car_string.split("-")
# cls() is the same as calling Car()
return cls(brand, model, int(price))
# Now you can create a car in a special way:
new_car = Car.from_string("Tesla-Model3-45000")
print(new_car.brand) # Tesla
Getters and Setters (@property)
Sometimes you want an attribute (like an email or a full name) to update automatically when other data changes. Using @property lets you call a method as if it were a simple variable.
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
@property
def fullname(self):
return f"{self.brand} {self.model}"
@fullname.setter
def fullname(self, name):
brand, model = name.split(" ")
self.brand = brand
self.model = model
my_car = Car("Ford", "Focus")
my_car.fullname = "BMW M3" # This triggers the setter!
print(my_car.brand) # BMW
Inheritance
Why rewrite code? If we want to make an Electric Car, we can just inherit from the Car class. It gets everything from the parent automatically.
super(): This tells Python to run the __init__ method of the parent class so we don’t have to re-type self.brand = brand, etc.
class Car:
def __init__(self, brand, model, price):
self.brand = brand
self.model = model
self.price = price
class ElectricCar(Car):
def __init__(self, brand, model, price, battery_size):
# Let the Parent (Car) handle the basic stuff
super().__init__(brand, model, price)
self.battery_size = battery_size
my_ev = ElectricCar("Tesla", "Model S", 80000, 100)
print(my_ev.brand) # Inherited from Car!
Inheriting Methods
When a child class (ElectricCar) inherits from a parent (Car), it gets every method for free.
class Car:
tax_rate = 1.05
def __init__(self, brand, price):
self.brand = brand
self.price = price
def description(self):
return f"A standard {self.brand}."
@staticmethod
def is_safe_speed(speed):
return speed < 120
class ElectricCar(Car):
def __init__(self, brand, price, battery_size):
super().__init__(brand, price)
self.battery_size = battery_size
# Testing Inheritance
my_ev = ElectricCar("Tesla", 80000, 100)
# Regular Method: Inherited!
print(my_ev.description()) # Output: A standard Tesla.
# Static Method: Inherited!
print(ElectricCar.is_safe_speed(100)) # Output: True
Overriding
Sometimes, the child needs to do things differently. This is called Overriding. If the child class defines a method with the exact same name as the parent, Python will use the child’s version.
class ElectricCar(Car):
# OVERRIDING a Regular Method
def description(self):
return f"A high-tech electric {self.brand}."
my_ev = ElectricCar("Tesla", 80000, 100)
print(my_ev.description()) # Output: A high-tech electric Tesla.
super() with Methods
Just like in the __init__, you can use super() to call the parent’s version of a method inside the child’s method. This is great when you want to do the parent’s action plus something extra.
class ElectricCar(Car):
def apply_maintenance(self):
# We might do generic car maintenance...
# super().apply_maintenance()
print("Checking battery health and motor software...")
Inheritance and Class Methods (cls)
Class methods are inherited too, but they have a special “superpower.” Because they use cls, they are aware of which class is calling them.
If you call a class method from ElectricCar, the cls argument will point to ElectricCar, not the original Car.
class Car:
tax_rate = 1.05
@classmethod
def show_tax(cls):
print(f"Tax for {cls.__name__} is {cls.tax_rate}")
class ElectricCar(Car):
tax_rate = 1.02 # Electric cars have lower tax!
# The parent method uses the child's data!
ElectricCar.show_tax() # Output: Tax for ElectricCar is 1.02
How can I check the family tree of a class?
# This shows you the search order: ElectricCar -> Car -> Object
print(ElectricCar.__mro__)
Inheritance of Properties
If the parent has a property, the child gets it automatically. You don’t have to do anything.
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
@property
def fullname(self):
return f"{self.brand} {self.model}"
class ElectricCar(Car):
pass
my_ev = ElectricCar("Tesla", "Model 3")
# The child uses the parent's property perfectly!
print(my_ev.fullname) # Output: Tesla Model 3
Overriding the Getter
Sometimes the child needs to display data differently. For example, maybe an ElectricCar should always show ”⚡” next to its name.
class ElectricCar(Car):
@property
def fullname(self):
# We use super() to get the standard name, then add our own flair
return f"⚡ {super().fullname}"
my_ev = ElectricCar("Tesla", "Model 3")
print(my_ev.fullname) # Output: ⚡ Tesla Model 3
Overriding the Setter (and using super())
This is where it gets interesting. Imagine the Car class has a rule: “Price cannot be negative.” The ElectricCar wants to keep that rule but adds its own: “Price cannot be less than $20,000” (because batteries are expensive).
In Python, if you want to override a setter in a child class, you usually have to re-declare the property in the child class to keep things clean.
class Car:
def __init__(self, price):
self._price = price
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative!")
self._price = value
class ElectricCar(Car):
@property
def price(self):
return super().price
@price.setter
def price(self, value):
if value < 20000:
raise ValueError("EVs cannot be cheaper than $20,000!")
# We use the parent's setter to check for negative numbers!
# This is how you call a setter through super()
Car.price.fset(self, value)
my_ev = ElectricCar(30000)
my_ev.price = 25000 # Works!
# my_ev.price = 15000 # Raises ValueError: EVs cannot be cheaper than $20,000!
What happens if you only override the setter in the child class?
If you only override the setter but don’t re-define the getter in the child, you might break the property.
In Python, when you define a setter in a child class with the same name as a property in the parent, it can ‘shadow’ or hide the parent’s property entirely. It is a best practice to re-define both the @property (getter) and the @name.setter in the child if you are changing how they work.