Classes and Objects

Learn the fundamentals of object-oriented programming in Python, including class definition, object creation, instance variables, methods, constructors, and special methods.

Ali Berro

By Ali Berro

14 min read Section 1
From: Python Fundamentals: From Zero to Hero

Classes and Objects

Object-Oriented Programming (OOP) is a programming paradigm that organizes code into objects, which are instances of classes. Python is an object-oriented language that supports classes and objects, allowing you to model real-world entities and their behaviors.

What is a Class?

A class is a blueprint or template for creating objects. It defines the attributes (data) and methods (functions) that the objects created from it will have.

What is an Object?

An object is an instance of a class. It’s a concrete realization of the class blueprint, with its own set of attribute values.

Think of a class as a cookie cutter and objects as the cookies made from it. The cookie cutter (class) defines the shape, while each cookie (object) can have different decorations (attribute values).

Defining a Class

To define a class in Python, use the class keyword followed by the class name:

basic-class.py
class Person:
pass # Empty class

Note

By convention, class names in Python use PascalCase (each word starts with a capital letter), e.g., Person, BankAccount, StudentRecord.

Creating Objects

To create an object (instance) from a class, call the class name like a function:

creating-objects.py
class Person:
pass
# Create objects (instances)
person1 = Person()
person2 = Person()
print(type(person1)) # <class '__main__.Person'>
print(person1) # <__main__.Person object at 0x...>

Instance Variables

Instance variables are attributes that belong to a specific instance (object). Each object has its own copy of instance variables.

You can add instance variables to an object by simply assigning values to them:

instance-variables.py
class Person:
pass
person1 = Person()
person1.name = "Alice"
person1.age = 25
person1.phone_number = "1234567890"
person2 = Person()
person2.name = "Bob"
person2.age = 30
print(person1.name, person1.age, person1.phone_number) # Alice 25
print(person2.name, person2.age) # Bob 30
# If we try to print person2.phone_number, it will raise an AttributeError

The __init__ Method (Constructor)

The __init__ method is a special method called a constructor. It’s automatically called when an object is created. Use it to initialize instance variables.

init-method.py
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)
print(person1.name, person1.age) # Alice 25
print(person2.name, person2.age) # Bob 30

Understanding self

The self parameter refers to the instance of the class. It’s automatically passed when you call a method on an object. You must include self as the first parameter of instance methods.

self-explanation.py
class Person:
def __init__(self, name):
self.name = name # self refers to the current instance
def introduce(self):
print(f"My name is {self.name}")
person = Person("Alice")
person.introduce() # My name is Alice
# When calling person.introduce(), Python automatically passes person as self

Instance Methods

Instance methods are functions defined inside a class that operate on instance variables. They must have self as their first parameter.

instance-methods.py
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
def display(self):
print(f"Rectangle: {self.length}x{self.width}")
print(f"Area: {self.area()}")
print(f"Perimeter: {self.perimeter()}")
rect = Rectangle(5, 3)
rect.display()
# Rectangle: 5x3
# Area: 15
# Perimeter: 16

Public, Protected, and Private Attributes

Python uses naming conventions to indicate the intended visibility of attributes and methods. Unlike some languages, Python doesn’t enforce strict access control, but these conventions help communicate intent.

Public Attributes

Public attributes are accessible from anywhere. By default, all attributes in Python are public (no special prefix):

public-attributes.py
class Person:
def __init__(self, name, age):
self.name = name # Public attribute
self.age = age # Public attribute
def display(self): # Public method
print(f"{self.name} is {self.age} years old")
person = Person("Alice", 25)
print(person.name) # Alice (accessible)
print(person.age) # 25 (accessible)
person.display() # Alice is 25 years old (accessible)

Protected Attributes

Protected attributes are indicated by a single underscore prefix _. This is a convention that signals “this is for internal use” but Python doesn’t prevent access:

protected-attributes.py
class BankAccount:
def __init__(self, balance):
self._balance = balance # Protected attribute (convention)
def get_balance(self):
return self._balance
def _validate_amount(self, amount): # Protected method
return amount > 0
account = BankAccount(1000)
print(account.get_balance()) # 1000 (using public method)
print(account._balance) # 1000 (accessible but not recommended)
print(account._validate_amount(50)) # True (accessible but not recommended)

Note

Protected attributes can still be accessed from outside the class, but the underscore prefix signals to other developers that these are intended for internal use and shouldn’t be accessed directly.

Private Attributes

Private attributes use a double underscore prefix __. Python performs name mangling on these attributes, making them harder (but not impossible) to access from outside the class:

private-attributes.py
class BankAccount:
def __init__(self, balance, account_number):
self.balance = balance # Public
self._account_number = account_number # Protected
self.__pin = "1234" # Private (name mangling)
def get_pin(self):
return self.__pin
def __validate_pin(self, pin): # Private method
return self.__pin == pin
account = BankAccount(1000, "ACC001")
print(account.balance) # 1000 (public - accessible)
print(account._account_number) # ACC001 (protected - accessible but not recommended)
print(account.get_pin()) # 1234 (using public method)
# print(account.__pin) # AttributeError: 'BankAccount' object has no attribute '__pin'
# print(account.__validate_pin("1234")) # AttributeError
# Name mangling: Python changes __pin to _BankAccount__pin
print(account._BankAccount__pin) # 1234 (not recommended, but possible)

How Name Mangling Works

When you use __attribute_name, Python automatically renames it to _ClassName__attribute_name:

name-mangling-demo.py
class MyClass:
def __init__(self):
self.public = "public"
self._protected = "protected"
self.__private = "private"
obj = MyClass()
print(obj.public) # public
print(obj._protected) # protected
# print(obj.__private) # AttributeError
# Name mangling in action
print(obj._MyClass__private) # private (mangled name)

When to Use Each

  • Public (no prefix): Use for attributes and methods that are part of the class’s public API
  • Protected (_single_underscore): Use for internal attributes/methods that subclasses might need
  • Private (__double_underscore): Use for attributes/methods that should not be accessed from outside, even by subclasses
access-control-example.py
class Employee:
def __init__(self, name, salary):
self.name = name # Public - part of API
self._department = "Engineering" # Protected - internal use
self.__salary = salary # Private - sensitive data
def get_salary(self): # Public method to access private data
return self.__salary
def _calculate_bonus(self): # Protected - internal calculation
return self.__salary * 0.1
def get_total_compensation(self): # Public method
return self.__salary + self._calculate_bonus()
emp = Employee("Alice", 50000)
print(emp.name) # Alice (public)
print(emp._department) # Engineering (protected - accessible)
print(emp.get_salary()) # 50000 (using public method)
print(emp.get_total_compensation()) # 55000.0
# print(emp.__salary) # AttributeError

Class Variables

Class variables are shared by all instances of a class. They are defined at the class level, outside of any method.

class-variables.py
class Dog:
species = "Canis familiaris" # Class variable (shared by all instances)
def __init__(self, name, breed):
self.name = name # Instance variable
self.breed = breed # Instance variable
dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Max", "German Shepherd")
print(dog1.species) # Canis familiaris
print(dog2.species) # Canis familiaris
print(Dog.species) # Canis familiaris (can access via class name)
# Modifying class variable affects all instances
Dog.species = "Canis lupus"
print(dog1.species) # Canis lupus
print(dog2.species) # Canis lupus

Class Methods

Class methods are methods that are bound to the class rather than an instance. They can access and modify class variables. Use the @classmethod decorator and cls as the first parameter.

class-methods.py
class Student:
total_students = 0 # Class variable
def __init__(self, name):
self.name = name
Student.total_students += 1
@classmethod
def get_total_students(cls):
return cls.total_students
@classmethod
def reset_count(cls):
cls.total_students = 0
student1 = Student("Alice")
student2 = Student("Bob")
student3 = Student("Charlie")
print(Student.get_total_students()) # 3
Student.reset_count()
print(Student.get_total_students()) # 0

Static Methods

Static methods are methods that don’t access instance or class data. They are utility functions related to the class but don’t need access to instance or class variables. Use the @staticmethod decorator.

static-methods.py
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Can be called on the class or instance
result1 = MathUtils.add(5, 3) # 8
result2 = MathUtils.multiply(4, 7) # 28
obj = MathUtils()
result3 = obj.add(10, 20) # 30 (also works on instance)

The __del__ Method (Destructor)

The __del__ method is called when an object is about to be destroyed. It’s rarely used in Python due to automatic garbage collection.

del-method.py
class Person:
def __init__(self, name):
self.name = name
print(f"{self.name} created")
def __del__(self):
print(f"{self.name} destroyed")
person1 = Person("Alice")
del person1 # Alice destroyed

Note

The __del__ method is not guaranteed to be called immediately when del is used, as Python uses garbage collection. It’s called when the object is actually destroyed by the garbage collector.

Special Methods (Magic Methods / Dunder Methods)

Python provides special methods (also called magic methods or dunder methods) that allow you to define how objects behave with built-in operations. They are surrounded by double underscores.

__str__ and __repr__

  • __str__: Returns a human-readable string representation (for end users)
  • __repr__: Returns an unambiguous string representation (for developers)
str-repr.py
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
person = Person("Alice", 25)
print(str(person)) # Alice, 25 years old
print(repr(person)) # Person('Alice', 25)
print(person) # Alice, 25 years old (calls __str__)

__len__

Defines the behavior of len() function:

len-method.py
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
print(len(cart)) # 2

__eq__ and __ne__

Define equality and inequality comparison:

eq-ne.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False
def __ne__(self, other):
return not self.__eq__(other)
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True
print(p1 == p3) # False
print(p1 != p3) # True

__lt__, __le__, __gt__, __ge__

Define comparison operators (<, <=, >, >=):

comparison-methods.py
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __lt__(self, other):
return self.grade < other.grade
def __le__(self, other):
return self.grade <= other.grade
def __gt__(self, other):
return self.grade > other.grade
def __ge__(self, other):
return self.grade >= other.grade
student1 = Student("Alice", 85)
student2 = Student("Bob", 90)
student3 = Student("Charlie", 85)
print(student1 < student2) # True
print(student1 > student2) # False
print(student1 <= student3) # True

__add__, __sub__, __mul__, etc.

Define arithmetic operations:

arithmetic-methods.py
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 4)
v3 = v1 + v2
v4 = v1 * 3
print(v3) # Vector(3, 7)
print(v4) # Vector(6, 9)

__getitem__ and __setitem__

Enable indexing and slicing:

getitem-setitem.py
class MyList:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def __len__(self):
return len(self.items)
my_list = MyList([1, 2, 3, 4, 5])
print(my_list[0]) # 1
print(my_list[1:4]) # [2, 3, 4]
my_list[2] = 10
print(my_list[2]) # 10

Properties

Properties allow you to define methods that can be accessed like attributes, providing a way to add getters, setters, and deleters.

properties.py
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 100
print(temp.celsius) # 37.777...

Complete Example

Here’s a complete example combining multiple concepts:

complete-example.py
class BankAccount:
# Class variable
account_count = 0
def __init__(self, owner, initial_balance=0):
self.owner = owner
self._balance = initial_balance # Private by convention
BankAccount.account_count += 1
self.account_number = BankAccount.account_count
def deposit(self, amount):
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
@property
def balance(self):
return self._balance
@classmethod
def get_account_count(cls):
return cls.account_count
@staticmethod
def validate_amount(amount):
return isinstance(amount, (int, float)) and amount > 0
def __str__(self):
return f"Account {self.account_number}: {self.owner}, Balance: ${self._balance:.2f}"
def __repr__(self):
return f"BankAccount('{self.owner}', {self._balance})"
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1) # Account 1: Alice, Balance: $1000.00
account1.deposit(200)
account1.withdraw(150)
print(account1.balance) # 1050.0
print(BankAccount.get_account_count()) # 2

Exercises

Exercise 1: Basic Class and Object

Create a class called Car with instance variables brand and model. Create an object and set these values, then print them.

Basic Class and Object

Checks: 0 times
Answer:
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
brand = input()
model = input()
car = Car(brand, model)
print(f"Brand: {car.brand}, Model: {car.model}")

Exercise 2: Instance Methods

Create a class called Circle with a constructor that takes radius. Add methods area() and circumference() that return the area and circumference of the circle. Read the radius from input and display both values.

Instance Methods

Checks: 0 times
Answer:
import math
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def circumference(self):
return 2 * math.pi * self.radius
radius = float(input())
circle = Circle(radius)
print(f"Area: {circle.area():.2f}")
print(f"Circumference: {circle.circumference():.2f}")

Exercise 3: Class Variables and Methods

Create a class called Book with a class variable total_books = 0. Increment this counter in the constructor. Add a class method get_total() that returns the total number of books created. Create 3 books and print the total.

Class Variables and Methods

Checks: 0 times
Answer:
class Book:
total_books = 0
def __init__(self, title):
self.title = title
Book.total_books += 1
@classmethod
def get_total(cls):
return cls.total_books
book1 = Book("Python Basics")
book2 = Book("Advanced Python")
book3 = Book("Python Mastery")
print(f"Total books: {Book.get_total()}")

Exercise 4: Special Methods

Create a class called Fraction with numerator and denominator. Implement __str__ to display as “numerator/denominator” and __eq__ to compare two fractions. Read two fractions and check if they are equal.

Special Methods

Checks: 0 times
Answer:
class Fraction:
def __init__(self, numerator, denominator):
self.numerator = numerator
self.denominator = denominator
def __str__(self):
return f"{self.numerator}/{self.denominator}"
def __eq__(self, other):
if isinstance(other, Fraction):
return (self.numerator * other.denominator ==
self.denominator * other.numerator)
return False
num1 = int(input())
den1 = int(input())
num2 = int(input())
den2 = int(input())
f1 = Fraction(num1, den1)
f2 = Fraction(num2, den2)
print(f1)
print(f2)
if f1 == f2:
print("Fractions are equal")
else:
print("Fractions are not equal")

Exercise 5: Properties

Create a class called Rectangle with private _width and _height. Use properties to get and set these values, ensuring they are always positive. Read width and height, create a rectangle, then read new values and update them.

Properties

Checks: 0 times
Answer:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value > 0:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
w1 = float(input())
h1 = float(input())
w2 = float(input())
h2 = float(input())
rect = Rectangle(w1, h1)
print(f"Width: {rect.width}, Height: {rect.height}")
rect.width = w2
rect.height = h2
print(f"Width: {rect.width}, Height: {rect.height}")

Course Progress

Section 49 of 61

Back to Course