Object-Oriented Programming
in Python
A Beginner's Guide
Table of Contents
1. Introduction to OOP 3
2. Functions in OOP 5
3. Access Specifiers 8
4. Getters and Setters 11
1. Introduction to Object-Oriented Programming
Object-Oriented Programming (OOP) is a programming paradigm that organizes code into
objects, which combine data (attributes) and functions (methods) that operate on that data.
Python is an object-oriented language, making it easy to create and work with classes and
objects.
1.1 What is a Class?
A class is a blueprint or template for creating objects. It defines the structure and behavior that
objects of that type will have. Think of it as a cookie cutter that shapes cookies.
1.2 What is an Object?
An object is an instance of a class. When you create an object from a class, you're
instantiating that class. Each object has its own set of attributes and can use the methods
defined in the class.
1.3 Basic Class Syntax
class Dog:
# Class attribute
species = "Canis familiaris"
# Constructor method
def __init__(self, name, age):
# Instance attributes
[Link] = name
[Link] = age
# Instance method
def bark(self):
return f"{[Link]} says Woof!"
# Creating objects
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
print([Link]) # Output: Buddy
print([Link]()) # Output: Buddy says Woof!
1.4 Key OOP Concepts
Concept Description
Encapsulation Bundling data and methods that work on that data within a class
Inheritance Creating new classes based on existing classes
Polymorphism Using a single interface to represent different types
Abstraction Hiding complex implementation details and showing only necessary features
1.5 The Constructor (__init__)
The __init__ method is a special method called a constructor. It's automatically called when
you create a new object. It initializes the object's attributes. The 'self' parameter refers to the
instance being created.
2. Functions in Object-Oriented Programming
In OOP, functions defined inside a class are called methods. They operate on the object's
data and define the object's behavior.
2.1 Instance Methods
Instance methods are the most common type of methods. They take 'self' as their first
parameter, which refers to the instance calling the method. They can access and modify
instance attributes.
class Calculator:
def __init__(self):
[Link] = 0
def add(self, num):
"""Instance method - operates on instance data"""
[Link] += num
return [Link]
def subtract(self, num):
[Link] -= num
return [Link]
def get_result(self):
return [Link]
# Using instance methods
calc = Calculator()
[Link](10) # result = 10
[Link](5) # result = 15
[Link](3) # result = 12
print(calc.get_result()) # Output: 12
2.2 Class Methods
Class methods are bound to the class rather than instances. They use the @classmethod
decorator and take 'cls' as their first parameter. They can access class attributes but not
instance attributes.
class Student:
school_name = "Python High School"
student_count = 0
def __init__(self, name):
[Link] = name
Student.student_count += 1
@classmethod
def get_school_name(cls):
"""Class method - operates on class data"""
return cls.school_name
@classmethod
def get_student_count(cls):
return cls.student_count
# Using class methods
print(Student.get_school_name()) # Output: Python High School
s1 = Student("Alice")
s2 = Student("Bob")
print(Student.get_student_count()) # Output: 2
2.3 Static Methods
Static methods don't receive any reference to the instance or class. They use the
@staticmethod decorator and behave like regular functions but belong to the class's
namespace.
class MathOperations:
@staticmethod
def add(x, y):
"""Static method - doesn't access instance or class data"""
return x + y
@staticmethod
def multiply(x, y):
return x * y
@staticmethod
def is_even(num):
return num % 2 == 0
# Using static methods
print([Link](5, 3)) # Output: 8
print(MathOperations.is_even(10)) # Output: True
2.4 Method Types Comparison
Method Type Decorator First Parameter Use Case
Instance Method None self Access/modify instance data
Class Method @classmethod cls Access/modify class data or create alternative constructors
Static Method @staticmethod None Utility functions related to the class
3. Access Specifiers in Python
Python uses naming conventions to indicate the intended access level of attributes and
methods. Unlike languages like Java or C++, Python doesn't enforce strict access control, but
follows conventions that developers respect.
3.1 Public Members
By default, all members are public. They can be accessed from anywhere, both inside and
outside the class. Public members have normal names without any leading underscores.
class Car:
def __init__(self, brand, model):
# Public attributes
[Link] = brand
[Link] = model
[Link] = 2024
# Public method
def display_info(self):
return f"{[Link]} {[Link]} {[Link]}"
# Accessing public members
car = Car("Toyota", "Camry")
print([Link]) # Output: Toyota
print(car.display_info()) # Output: 2024 Toyota Camry
[Link] = 2023 # Can modify public attributes
3.2 Protected Members
Protected members are indicated by a single leading underscore (_). This is a convention that
suggests the member should only be accessed within the class and its subclasses. Python
doesn't enforce this, but it's a signal to other developers.
class BankAccount:
def __init__(self, account_number, balance):
self.account_number = account_number
# Protected attribute
self._balance = balance
# Protected method
def _validate_amount(self, amount):
return amount > 0
def deposit(self, amount):
if self._validate_amount(amount):
self._balance += amount
return True
return False
# Using protected members
account = BankAccount("12345", 1000)
# Technically accessible but discouraged
print(account._balance) # Output: 1000 (but shouldn't do this)
# Better approach: use public methods
[Link](500)
3.3 Private Members
Private members are indicated by a double leading underscore (__). Python performs name
mangling on these members, making them harder to access from outside the class. They
should only be accessed within the class itself.
class SecureData:
def __init__(self, username, password):
[Link] = username
# Private attribute
self.__password = password
# Private method
def __encrypt_password(self):
# Simple example (not real encryption)
return "***" + self.__password[-3:]
def display_secure_info(self):
return f"User: {[Link]}, Pass: {self.__encrypt_password()}"
# Using private members
data = SecureData("john_doe", "secret123")
print([Link]) # Output: john_doe
# print(data.__password) # AttributeError!
print(data.display_secure_info()) # Output: User: john_doe, Pass: ***123
# Name mangling allows access (but shouldn't be used)
# print(data._SecureData__password) # Output: secret123
3.4 Access Specifiers Summary
Type Notation Example Access Level
Public name [Link] Accessible everywhere
Protected _name self._name Should only access in class and subclasses
Private __name self.__name Should only access within class (name mangled)
Important Note: Python's philosophy is 'We are all consenting adults here.' Access specifiers
are conventions to guide developers, not strict enforcement mechanisms. Respect these
conventions in your code!
4. Getters and Setters in Python
Getters and setters are methods used to access and modify private attributes. They provide
controlled access to class data and allow you to add validation logic. Python provides the
@property decorator to create Pythonic getters and setters.
4.1 Traditional Getter and Setter Methods
The traditional approach uses explicit get and set methods. While this works, it's not very
Pythonic.
class Employee:
def __init__(self, name, salary):
self.__name = name
self.__salary = salary
# Getter methods
def get_name(self):
return self.__name
def get_salary(self):
return self.__salary
# Setter methods
def set_name(self, name):
if len(name) > 0:
self.__name = name
else:
raise ValueError("Name cannot be empty")
def set_salary(self, salary):
if salary >= 0:
self.__salary = salary
else:
raise ValueError("Salary must be positive")
# Using traditional getters and setters
emp = Employee("Alice", 50000)
print(emp.get_name()) # Output: Alice
emp.set_salary(55000)
print(emp.get_salary()) # Output: 55000
4.2 Using @property Decorator (Pythonic Way)
The @property decorator allows you to define getters, setters, and deleters that can be
accessed like regular attributes. This is the preferred approach in Python as it provides
cleaner syntax.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
# Getter using @property
@property
def celsius(self):
"""Get temperature in Celsius"""
return self._celsius
# Setter using @property_name.setter
@[Link]
def celsius(self, value):
"""Set temperature in Celsius with validation"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
# Additional computed property
@property
def fahrenheit(self):
"""Get temperature in Fahrenheit"""
return (self._celsius * 9/5) + 32
@[Link]
def fahrenheit(self, value):
"""Set temperature using Fahrenheit"""
self._celsius = (value - 32) * 5/9
# Using property decorators
temp = Temperature(25)
print([Link]) # Output: 25 (calls getter)
print([Link]) # Output: 77.0 (computed property)
[Link] = 30 # Calls setter
print([Link]) # Output: 30
[Link] = 86 # Set via Fahrenheit
print([Link]) # Output: 30.0
4.3 Complete Example with Validation
Here's a comprehensive example showing how getters and setters provide encapsulation and
data validation.
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
self._email = ""
@property
def name(self):
"""Get person's name"""
return self._name
@[Link]
def name(self, value):
"""Set name with validation"""
if not value or not isinstance(value, str):
raise ValueError("Name must be a non-empty string")
self._name = [Link]()
@property
def age(self):
"""Get person's age"""
return self._age
@[Link]
def age(self, value):
"""Set age with validation"""
if not isinstance(value, int) or value < 0 or value > 150:
raise ValueError("Age must be between 0 and 150")
self._age = value
@property
def email(self):
"""Get email address"""
return self._email
@[Link]
def email(self, value):
"""Set email with basic validation"""
if "@" not in value:
raise ValueError("Invalid email address")
self._email = value
@property
def info(self):
"""Read-only computed property"""
return f"{self._name} ({self._age} years old)"
# Using the Person class
person = Person("John Doe", 30)
print([Link]) # Output: John Doe
print([Link]) # Output: 30
[Link] = "Jane Smith"
[Link] = 25
[Link] = "jane@[Link]"
print([Link]) # Output: Jane Smith (25 years old)
# Validation in action
try:
[Link] = 200 # Raises ValueError
except ValueError as e:
print(f"Error: {e}") # Output: Error: Age must be between 0 and 150
4.4 Benefits of Getters and Setters
Benefit Description
Encapsulation Hide internal representation and control access to data
Validation Ensure data integrity by validating values before setting
Computed Properties Calculate values on-the-fly without storing them
Flexibility Change internal implementation without breaking external code
Debugging Add logging or breakpoints to track attribute access
Read-Only Properties Create attributes that can be read but not modified
4.5 Best Practices
1. Use @property for computed attributes: When an attribute's value is derived from other
attributes.
2. Add validation in setters: Ensure data integrity by validating input values.
3. Keep getters simple: Getters should be fast and not perform expensive operations.
4. Create read-only properties: Define only @property without a setter for read-only
attributes.
5. Follow naming conventions: Use single underscore prefix for protected attributes that
have properties.