AiTechWorlds
AiTechWorlds
Walk into any car factory and you will find a master specification sheet pinned to the wall. It records two very different kinds of information.
Factory-level facts — things true of every car this factory will ever produce: the factory name, the production line number, the total count of cars manufactured so far. These belong to the factory, not to any individual car.
Car-level facts — things that vary from car to car: the specific make, model, year, colour, and chassis number. These belong to each individual vehicle.
And then there are actions every car can perform: accelerate, brake, honk. Some actions are done by an individual car. Some are done by the factory on behalf of all cars. Some are pure calculations that don't need any car or factory context at all.
Python classes mirror this structure exactly.
class Car:
# Class attribute — shared by ALL Car objects
factory_name = "PyMotors Inc."
total_produced = 0 # Tracks every car ever made
def __init__(self, make, model, year):
# Instance attributes — unique to THIS specific car
self.make = make
self.model = model
self.year = year
Car.total_produced += 1 # Update the factory counter on creation
def describe(self):
return f"{self.year} {self.make} {self.model}"
car1 = Car("Toyota", "Camry", 2022)
car2 = Car("Ford", "Mustang", 2023)
car3 = Car("Tesla", "Model 3", 2024)
print(car1.describe()) # 2022 Toyota Camry
print(car2.describe()) # 2023 Ford Mustang
print(Car.total_produced) # 3 — accessed on the CLASS, not an instance
print(car1.factory_name) # PyMotors Inc. — accessible via instance too
Output:
2022 Toyota Camry
2023 Ford Mustang
3
PyMotors Inc.
factory_name and total_produced live on the class itself — one copy, shared by all objects.self.make, self.model, self.year are created fresh for every new Car() call.Car.total_produced += 1 updates the class attribute via the class name — using self.total_produced += 1 would accidentally create a per-instance copy instead.__init__, __str__, and __repr__These three dunder methods (double-underscore) are the most important ones to learn first. They tell Python how to create and display your objects.
class Employee:
def __init__(self, name, department, salary):
"""Called automatically when Employee(...) is invoked."""
self.name = name
self.department = department
self.salary = salary
def __str__(self):
"""
Called by print() and str().
Write this for HUMANS — make it readable.
"""
return f"Employee: {self.name} ({self.department})"
def __repr__(self):
"""
Called in the REPL and by repr().
Write this for DEVELOPERS — make it unambiguous and reproducible.
"""
return f"Employee(name={self.name!r}, department={self.department!r}, salary={self.salary})"
emp = Employee("Sarah", "Engineering", 95000)
print(emp) # Uses __str__
print(repr(emp)) # Uses __repr__
Output:
Employee: Sarah (Engineering)
Employee(name='Sarah', department='Engineering', salary=95000)
Rule of thumb:
__str__is for users of your program;__repr__is for you and other developers debugging it.
| Method Type | Decorator | First Parameter | Receives | When to Use |
|---|---|---|---|---|
| Instance method | (none) | self | The specific object | Most things — operates on object data |
| Class method | @classmethod | cls | The class itself | Alternative constructors, factory methods |
| Static method | @staticmethod | (none) | Nothing automatically | Pure utility functions related to the class |
class Employee:
company = "TechCorp"
_headcount = 0
def __init__(self, name, department, salary):
self.name = name
self.department = department
self.salary = salary
Employee._headcount += 1
def __str__(self):
return f"Employee: {self.name} ({self.department})"
def __repr__(self):
return f"Employee(name={self.name!r}, department={self.department!r}, salary={self.salary})"
# --- Instance method: works on THIS employee ---
def give_raise(self, percent):
"""Increase this employee's salary by a percentage."""
self.salary *= (1 + percent / 100)
print(f"{self.name} now earns ${self.salary:,.2f}")
# --- Class method: works on the class, not an instance ---
@classmethod
def from_dict(cls, data):
"""
Alternative constructor — create an Employee from a dictionary.
'cls' is the class itself (Employee), so cls(...) creates an instance.
"""
return cls(data["name"], data["department"], data["salary"])
@classmethod
def headcount(cls):
"""Return the total number of employees created."""
return cls._headcount
# --- Static method: utility with no access to class or instance ---
@staticmethod
def is_valid_salary(salary):
"""Pure calculation — doesn't need self or cls."""
return isinstance(salary, (int, float)) and salary > 0
# Using the instance method
emp1 = Employee("Sarah", "Engineering", 95000)
emp1.give_raise(10)
# Using the class method as an alternative constructor
data = {"name": "James", "department": "Marketing", "salary": 72000}
emp2 = Employee.from_dict(data)
print(emp2)
# Using the class method to check headcount
print(f"Total employees: {Employee.headcount()}")
# Using the static method
print(Employee.is_valid_salary(50000)) # True
print(Employee.is_valid_salary(-100)) # False
print(Employee.is_valid_salary("free")) # False
Output:
Sarah now earns $104,500.00
Employee: James (Marketing)
Total employees: 2
True
False
False
Method chaining lets you call multiple methods in one expression by having each method return self.
class QueryBuilder:
def __init__(self):
self._table = ""
self._conditions = []
self._limit = None
def from_table(self, table):
self._table = table
return self # Return self to allow chaining
def where(self, condition):
self._conditions.append(condition)
return self
def limit(self, n):
self._limit = n
return self
def build(self):
query = f"SELECT * FROM {self._table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
if self._limit:
query += f" LIMIT {self._limit}"
return query
# Chained calls — reads almost like natural language
query = (QueryBuilder()
.from_table("users")
.where("age > 18")
.where("active = true")
.limit(10)
.build())
print(query)
Output:
SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10
Each method returns self, so the next method call operates on the same object. The final .build() does not return self — it returns the finished string.
__init__ constructs, __str__ speaks to humans, __repr__ speaks to developers.return self) creates readable, fluent APIs.Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises