Complete Python Programming Guide
1. Features of Python & OOP Concepts Features of Python: Easy to Learn & Read: Simple syntax similar to English Interpreted Language: Executes line by line, no compilation needed Dynamically Typed: No need to declare variable types Object-Oriente...

1. Features of Python & OOP Concepts
Features of Python:
Easy to Learn & Read: Simple syntax similar to English
Interpreted Language: Executes line by line, no compilation needed
Dynamically Typed: No need to declare variable types
Object-Oriented: Supports OOP concepts
Large Standard Library: Built-in modules for various tasks
Platform Independent: Works on Windows, Mac, Linux
Free & Open Source
OOP Concepts in Python:

Data Types in Python:
1. Numeric Types:
x = 10 # int (integer)
y = 10.5 # float (decimal number)
z = 2 + 3j # complex number
2. Sequence Types:
my_string = "Hello" # str (string)
my_list = [1, 2, 3] # list (mutable, ordered)
my_tuple = (1, 2, 3) # tuple (immutable, ordered)
3. Set Types:
my_set = {1, 2, 3} # set (unordered, unique elements)
my_frozenset = frozenset([1, 2, 3]) # immutable set
4. Mapping Type:
my_dict = {"name": "John", "age": 25} # dictionary (key-value pairs)
5. Boolean Type:
is_active = True # bool (True or False)
6. None Type:
value = None # NoneType (represents absence of value)
2. Functions & Parameters
What is a Function?
A function is a reusable block of code that performs a specific task. It helps avoid code repetition.
def greet(): # Function definition
print("Hello!")
greet() # Function call
Formal Parameter vs Actual Parameter:
Formal Parameters: Variables in function definition Actual Parameters: Actual values passed when calling function
def add(a, b): # a, b are formal parameters
return a + b
result = add(5, 3) # 5, 3 are actual parameters (arguments)
Types of Arguments:
1. Positional Arguments:
def person_info(name, age):
print(f"Name: {name}, Age: {age}")
person_info("Alice", 25) # Order matters
# Output: Name: Alice, Age: 25
2. Default Arguments:
def greet(name, message="Hello"): # message has default value
print(f"{message}, {name}!")
greet("Bob") # Hello, Bob!
greet("Bob", "Hi") # Hi, Bob!
3. Keyword Arguments:
def display(name, age, city):
print(f"{name}, {age}, {city}")
display(age=30, name="John", city="NYC") # Order doesn't matter
4. Variable Length Arguments:
*Using args (for multiple positional arguments):
def sum_all(*numbers): # Accepts any number of arguments
total = 0
for num in numbers:
total += num
return total
print(sum_all(1, 2, 3, 4, 5)) # Output: 15
**Using kwargs (for multiple keyword arguments):
def student_info(**details): # Accepts any number of key-value pairs
for key, value in details.items():
print(f"{key}: {value}")
student_info(name="Alice", age=20, grade="A")
# Output:
# name: Alice
# age: 20
# grade: A
3. Dictionary, Conditional Statements & Loops
Dictionary:
A dictionary stores data in key-value pairs. Keys must be unique and immutable.
Creating a Dictionary:
# Method 1: Using curly braces
student = {"name": "Alice", "age": 20, "grade": "A"}
# Method 2: Using dict() constructor
student = dict(name="Alice", age=20, grade="A")
# Empty dictionary
empty_dict = {}
Dictionary Methods:
student = {"name": "Alice", "age": 20, "grade": "A"}
# Accessing values
print(student["name"]) # Alice
print(student.get("age")) # 20
# Adding/Updating
student["city"] = "NYC" # Add new key-value
student["age"] = 21 # Update existing
# Removing items
student.pop("grade") # Remove specific key
del student["city"] # Remove using del
student.clear() # Remove all items
# Other methods
student = {"name": "Alice", "age": 20}
print(student.keys()) # dict_keys(['name', 'age'])
print(student.values()) # dict_values(['Alice', 20])
print(student.items()) # dict_items([('name', 'Alice'), ('age', 20)])
# Update dictionary
student.update({"grade": "A", "city": "NYC"})
Conditional Statements:
If-Else:
age = 18
if age >= 18:
print("Adult")
else:
print("Minor")
Nested If:
marks = 85
if marks >= 50:
print("Pass")
if marks >= 75:
print("Distinction")
else:
print("First Class")
else:
print("Fail")
Elif (else if):
marks = 85
if marks >= 90:
print("Grade: A+")
elif marks >= 75:
print("Grade: A")
elif marks >= 60:
print("Grade: B")
else:
print("Grade: C")
Loops:
For Loop:
# Iterate through a list
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# Using range()
for i in range(5): # 0 to 4
print(i)
# For loop with dictionary
student = {"name": "Alice", "age": 20}
for key, value in student.items():
print(f"{key}: {value}")
For Loop with Else:
for i in range(5):
print(i)
else:
print("Loop completed") # Executes after loop finishes
Break, Continue, Pass:
Break: Exits the loop completely
for i in range(10):
if i == 5:
break # Stop loop when i is 5
print(i)
# Output: 0, 1, 2, 3, 4
Continue: Skips current iteration, continues with next
for i in range(5):
if i == 2:
continue # Skip when i is 2
print(i)
# Output: 0, 1, 3, 4
Pass: Does nothing, placeholder
for i in range(5):
if i == 2:
pass # Does nothing
print(i)
# Output: 0, 1, 2, 3, 4
4. Variable Scope, Modules & OOP Concepts
Local vs Global Variables:
Local Variable: Declared inside a function, accessible only within that function
def my_function():
x = 10 # Local variable
print(x)
my_function() # Output: 10
# print(x) # Error: x is not defined outside function
Global Variable: Declared outside function, accessible everywhere
x = 10 # Global variable
def my_function():
print(x) # Can access global variable
my_function() # Output: 10
print(x) # Output: 10
Modifying Global Variable inside Function:
x = 10
def modify():
global x # Declare x as global
x = 20
modify()
print(x) # Output: 20
Break vs Continue:
| Break | Continue |
| Exits the loop completely | Skips current iteration only |
| Loop terminates | Loop continues with next iteration |
| Used to stop loop when condition met | Used to skip specific iterations |
# Break example
for i in range(10):
if i == 5:
break
print(i) # Output: 0, 1, 2, 3, 4
# Continue example
for i in range(5):
if i == 2:
continue
print(i) # Output: 0, 1, 3, 4
Module vs Package:
| Module | Package |
| A single Python file (.py) | A collection of modules |
| Contains functions, classes, variables | Contains multiple modules in a directory |
| Example: math.py | Example: numpy, pandas |
Using a Module:
# math is a built-in module
import math
print(math.sqrt(16)) # Output: 4.0
# Import specific function
from math import sqrt
print(sqrt(25)) # Output: 5.0
Creating Your Own Module:
# File: mymodule.py
def greet(name):
return f"Hello, {name}!"
# Using the module
import mymodule
print(mymodule.greet("Alice"))
Method Overloading vs Method Overriding:
| Method Overloading | Method Overriding |
| Multiple methods with same name but different parameters | Child class redefines parent class method |
| Python doesn't support true overloading | Python supports overriding |
| Uses default arguments as workaround | Uses inheritance |
Method Overriding Example:
class Animal:
def sound(self):
print("Animal makes sound")
class Dog(Animal):
def sound(self): # Overriding parent method
print("Dog barks")
d = Dog()
d.sound() # Output: Dog barks
Method Overloading Workaround:
class Calculator:
def add(self, a, b=0, c=0): # Using default arguments
return a + b + c
calc = Calculator()
print(calc.add(5)) # Output: 5
print(calc.add(5, 3)) # Output: 8
print(calc.add(5, 3, 2)) # Output: 10
5. Programming Concepts Comparison
Iteration vs Recursion:
| Iteration | Recursion |
| Repeats using loops | Function calls itself |
| Uses for/while loops | Uses function calls |
| Generally faster | Can be slower due to function calls |
| More memory efficient | Uses more memory (call stack) |
Iteration Example:
def factorial_iterative(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
print(factorial_iterative(5)) # Output: 120
Recursion Example:
def factorial_recursive(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial_recursive(n - 1)
print(factorial_recursive(5)) # Output: 120
Call by Value vs Call by Reference:
Python uses "Call by Object Reference" (similar to call by reference for mutable objects)
# Immutable object (behaves like call by value)
def modify_number(x):
x = x + 10
print("Inside function:", x)
num = 5
modify_number(num)
print("Outside function:", num)
# Output:
# Inside function: 15
# Outside function: 5 (unchanged)
# Mutable object (behaves like call by reference)
def modify_list(lst):
lst.append(4)
print("Inside function:", lst)
my_list = [1, 2, 3]
modify_list(my_list)
print("Outside function:", my_list)
# Output:
# Inside function: [1, 2, 3, 4]
# Outside function: [1, 2, 3, 4] (changed)
List vs Tuple:
| List | Tuple |
| Mutable (can be changed) | Immutable (cannot be changed) |
| Uses square brackets [ ] | Uses parentheses ( ) |
| Slower | Faster |
| More memory | Less memory |
| Methods: append, remove, etc. | Limited methods |
# List
my_list = [1, 2, 3]
my_list[0] = 10 # Allowed
my_list.append(4) # Allowed
print(my_list) # Output: [10, 2, 3, 4]
# Tuple
my_tuple = (1, 2, 3)
# my_tuple[0] = 10 # Error: Cannot modify
print(my_tuple) # Output: (1, 2, 3)
Abstract Class vs Interface:
| Abstract Class | Interface |
| Can have abstract and concrete methods | All methods are abstract (in pure interface) |
| Can have instance variables | Cannot have instance variables (in pure interface) |
| Uses ABC module | Python doesn't have true interfaces |
| Single inheritance | Multiple inheritance possible |
Abstract Class Example:
from abc import ABC, abstractmethod
class Vehicle(ABC): # Abstract class
def __init__(self, brand):
self.brand = brand # Concrete attribute
@abstractmethod
def start(self): # Abstract method
pass
def display(self): # Concrete method
print(f"Brand: {self.brand}")
class Car(Vehicle):
def start(self): # Must implement abstract method
print("Car started")
c = Car("Toyota")
c.start() # Car started
c.display() # Brand: Toyota
Interface-like Structure (using ABC):
from abc import ABC, abstractmethod
class Printable(ABC): # Interface-like
@abstractmethod
def print_document(self):
pass
class Scannable(ABC): # Interface-like
@abstractmethod
def scan_document(self):
pass
class Printer(Printable, Scannable): # Multiple inheritance
def print_document(self):
print("Printing...")
def scan_document(self):
print("Scanning...")
Mutable vs Immutable Data Types:
| Mutable | Immutable |
| Can be changed after creation | Cannot be changed after creation |
| list, dict, set | int, float, str, tuple, frozenset |
| Modified in place | Creates new object |
# Mutable (List)
my_list = [1, 2, 3]
my_list[0] = 10 # Changes original list
print(my_list) # [10, 2, 3]
# Immutable (Tuple)
my_tuple = (1, 2, 3)
# my_tuple[0] = 10 # Error
# Immutable (String)
text = "Hello"
# text[0] = 'h' # Error
new_text = text.replace('H', 'h') # Creates new string
print(text) # Hello (unchanged)
print(new_text) # hello (new string)
6. Short Notes on Key Concepts
Data Hiding (Encapsulation):
Restricting access to certain attributes/methods to prevent accidental modification.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute (double underscore)
def deposit(self, amount):
self.__balance += amount
def get_balance(self):
return self.__balance
account = BankAccount(1000)
# print(account.__balance) # Error: Cannot access directly
print(account.get_balance()) # 1000 (Access through method)
Polymorphism:
Same method name behaving differently based on context.
Types:
Compile-time (Method Overloading) - Not directly supported in Python
Runtime (Method Overriding) - Supported
# Runtime Polymorphism
class Shape:
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
# Same method name, different behavior
c = Circle(5)
r = Rectangle(4, 6)
print(c.area()) # 78.5
print(r.area()) # 24
Local, Nonlocal, Global Variables:
x = "global" # Global variable
def outer():
x = "enclosing" # Enclosing variable
def inner():
x = "local" # Local variable
print("Local:", x)
inner()
print("Enclosing:", x)
outer()
print("Global:", x)
# Using nonlocal to modify enclosing variable
def outer():
x = "enclosing"
def inner():
nonlocal x # Refers to enclosing scope
x = "modified"
inner()
print(x) # Output: modified
outer()
globals() Function:
Returns a dictionary of global variables.
x = 10
y = 20
print(globals()) # Shows all global variables
print(globals()['x']) # Access specific global variable: 10
Operators in Python:
1. Arithmetic Operators: +, -, , /, //, %, *
print(10 + 3) # 13 (Addition)
print(10 - 3) # 7 (Subtraction)
print(10 * 3) # 30 (Multiplication)
print(10 / 3) # 3.333... (Division)
print(10 // 3) # 3 (Floor division)
print(10 % 3) # 1 (Modulus)
print(10 ** 3) # 1000 (Exponent)
2. Comparison Operators: ==, !=, >, <, >=, <=
print(5 == 5) # True
print(5 != 3) # True
print(5 > 3) # True
3. Logical Operators: and, or, not
print(True and False) # False
print(True or False) # True
print(not True) # False
4. Assignment Operators: =, +=, -=, *=, /=
x = 10
x += 5 # x = x + 5
print(x) # 15
5. Identity Operators: is, is not
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a is c) # True (same object)
print(a is b) # False (different objects)
print(a == b) # True (same value)
6. Membership Operators: in, not in
print(3 in [1, 2, 3]) # True
print('a' in "apple") # True
7. Bitwise Operators: &, |, ^, ~, <<, >>
print(5 & 3) # 1 (AND)
print(5 | 3) # 7 (OR)
print(5 ^ 3) # 6 (XOR)
Operator Precedence & Associativity:
Precedence (Highest to Lowest):
** (Exponent)
*, /, //, % (Multiplication, Division)
+, - (Addition, Subtraction)
<, <=, >, >= (Comparison)
\==, != (Equality)
and, or (Logical)
Associativity:
Most operators: Left to Right
Exponent (**): Right to Left
# Precedence
result = 2 + 3 * 4 # Multiplication first
print(result) # 14 (not 20)
# Associativity
result = 2 ** 3 ** 2 # Right to left
print(result) # 512 (2 ** 9, not 8 ** 2)
Type Conversion:
Implicit Conversion (Automatic):
x = 10 # int
y = 3.5 # float
result = x + y # int converts to float automatically
print(result) # 13.5 (float)
Explicit Conversion (Manual):
# String to int
x = int("10")
print(x) # 10
# Int to string
y = str(100)
print(y) # "100"
# String to float
z = float("3.14")
print(z) # 3.14
# List to tuple
my_list = [1, 2, 3]
my_tuple = tuple(my_list)
print(my_tuple) # (1, 2, 3)
7. Advanced Concepts
Tail Recursion:
Recursion where recursive call is the last operation in the function.
# Non-tail recursion
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1) # Multiplication after recursive call
# Tail recursion (with helper function)
def factorial_tail(n, accumulator=1):
if n == 1:
return accumulator
return factorial_tail(n - 1, n * accumulator) # Recursive call is last
print(factorial(5)) # 120
print(factorial_tail(5)) # 120
self Keyword:
Refers to the current instance of the class. Used to access instance variables and methods.
class Person:
def __init__(self, name, age):
self.name = name # self refers to the current object
self.age = age
def display(self):
print(f"Name: {self.name}, Age: {self.age}")
p1 = Person("Alice", 25)
p2 = Person("Bob", 30)
p1.display() # self = p1
p2.display() # self = p2
Class Variable vs Instance Variable:
class Employee:
company = "ABC Corp" # Class variable (shared by all instances)
def __init__(self, name, salary):
self.name = name # Instance variable (unique to each instance)
self.salary = salary
e1 = Employee("Alice", 50000)
e2 = Employee("Bob", 60000)
print(e1.company) # ABC Corp (same for all)
print(e2.company) # ABC Corp
print(e1.name) # Alice (different for each)
print(e2.name) # Bob
# Changing class variable
Employee.company = "XYZ Corp"
print(e1.company) # XYZ Corp
print(e2.company) # XYZ Corp
super() Keyword:
Calls methods from parent class.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print("Animal speaks")
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call parent constructor
self.breed = breed
def speak(self):
super().speak() # Call parent method
print("Dog barks")
d = Dog("Buddy", "Golden Retriever")
d.speak()
# Output:
# Animal speaks
# Dog barks
Static Method:
Method that doesn't require instance or class. Defined using @staticmethod.
class MathOperations:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Call without creating instance
print(MathOperations.add(5, 3)) # 8
print(MathOperations.multiply(4, 2)) # 8
Tuple Assignment:
Assigning multiple values at once.
# Basic tuple assignment
x, y, z = 10, 20, 30
print(x, y, z) # 10 20 30
# Swapping values
a, b = 5, 10
a, b = b, a # Swap
print(a, b) # 10 5
# Unpacking
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
Identifier & Token:
Identifier: Names given to variables, functions, classes, etc.
# Valid identifiers
name = "Alice"
age2 = 25
_value = 100
# Invalid identifiers
# 2age = 25 # Cannot start with number
# my-name = "Bob" # Cannot use hyphen
Rules for Identifiers:
Must start with letter (a-z, A-Z) or underscore (_)
Can contain letters, digits, underscore
Case sensitive (age and Age are different)
Cannot be a keyword (if, else, for, etc.)
Token: Smallest unit in a program Types:
Keywords (if, else, for, while, etc.)
Identifiers (variable names, function names)
Literals (10, "hello", 3.14)
Operators (+, -, *, /)
Delimiters ((), [], {}, :, ,)
8. Constructors
What is a Constructor?
Special method that initializes an object when it's created. Always named __init__().
class Person:
def __init__(self, name, age): # Constructor
self.name = name
self.age = age
p = Person("Alice", 25) # Constructor called automatically
Features of Constructor:
Automatic Invocation: Called automatically when object is created
Name is Fixed: Always
__init__()Initialize Attributes: Sets initial values for object attributes
No Return Value: Cannot return any value
Can be Overloaded: Using default arguments
Types of Constructors:
1. Default Constructor: Constructor with no parameters (except self).
class Student:
def __init__(self): # Default constructor
self.name = "Unknown"
self.age = 0
print("Default constructor called")
s = Student()
print(s.name) # Unknown
print(s.age) # 0
2. Parameterized Constructor: Constructor that accepts parameters to initialize object with specific values.
class Student:
def __init__(self, name, age, grade): # Parameterized constructor
self.name = name
self.age = age
self.grade = grade
print("Parameterized constructor called")
def display(self):
print(f"Name: {self.name}, Age: {self.age}, Grade: {self.grade}")
# Creating objects with different values
s1 = Student("Alice", 20, "A")
s2 = Student("Bob", 22, "B")
s1.display() # Name: Alice, Age: 20, Grade: A
s2.display() # Name: Bob, Age: 22, Grade: B
Parameterized Constructor with Default Values:
class Product:
def __init__(self, name, price=0, quantity=1):
self.name = name
self.price = price
self.quantity = quantity
def display(self):
print(f"{self.name}: ${self.price} x {self.quantity}")
p1 = Product("Laptop", 1000, 2)
p2 = Product("Mouse") # Uses default price and quantity
p1.display() # Laptop: $1000 x 2
p2.display() # Mouse: $0 x 1
append() vs extend():
| append() | extend() |
| Adds single element | Adds multiple elements |
| Adds element as-is | Iterates and adds each element |
| Increases length by 1 | Increases length by number of elements added |
# append()
list1 = [1, 2, 3]
list1.append(4)
print(list1) # [1, 2, 3, 4]
list1.append([5, 6]) # Adds entire list as single element
print(list1) # [1, 2, 3, 4, [5, 6]]
# extend()
list2 = [1, 2, 3]
Complete Python Programming Guide
1. Features of Python & OOP Concepts
Features of Python:
Easy to Learn & Read: Simple syntax similar to English
Interpreted Language: Executes line by line, no compilation needed
Dynamically Typed: No need to declare variable types
Object-Oriented: Supports OOP concepts
Large Standard Library: Built-in modules for various tasks
Platform Independent: Works on Windows, Mac, Linux
Free & Open Source
OOP Concepts in Python:
1. Class & Object:
class Car:
def __init__(self, brand, model):
self.brand = brand # Instance variable
self.model = model
def display(self):
print(f"{self.brand} {self.model}")
# Creating object
my_car = Car("Toyota", "Camry")
my_car.display() # Output: Toyota Camry
2. Encapsulation (Data Hiding):
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private variable (double underscore)
def get_balance(self):
return self.__balance
def deposit(self, amount):
self.__balance += amount
3. Inheritance:
class Animal: # Parent class
def speak(self):
print("Animal speaks")
class Dog(Animal): # Child class inherits from Animal
def bark(self):
print("Dog barks")
d = Dog()
d.speak() # Inherited method
d.bark() # Own method
4. Polymorphism:
class Bird:
def fly(self):
print("Bird flies")
class Airplane:
def fly(self):
print("Airplane flies")
# Same method name, different behavior
b = Bird()
a = Airplane()
b.fly() # Bird flies
a.fly() # Airplane flies
5. Abstraction:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
Data Types in Python:
1. Numeric Types:
x = 10 # int (integer)
y = 10.5 # float (decimal number)
z = 2 + 3j # complex number
2. Sequence Types:
my_string = "Hello" # str (string)
my_list = [1, 2, 3] # list (mutable, ordered)
my_tuple = (1, 2, 3) # tuple (immutable, ordered)
3. Set Types:
my_set = {1, 2, 3} # set (unordered, unique elements)
my_frozenset = frozenset([1, 2, 3]) # immutable set
4. Mapping Type:
my_dict = {"name": "John", "age": 25} # dictionary (key-value pairs)
5. Boolean Type:
is_active = True # bool (True or False)
6. None Type:
value = None # NoneType (represents absence of value)
2. Functions & Parameters
What is a Function?
A function is a reusable block of code that performs a specific task. It helps avoid code repetition.
def greet(): # Function definition
print("Hello!")
greet() # Function call
Formal Parameter vs Actual Parameter:
Formal Parameters: Variables in function definition Actual Parameters: Actual values passed when calling function
def add(a, b): # a, b are formal parameters
return a + b
result = add(5, 3) # 5, 3 are actual parameters (arguments)
Types of Arguments:
1. Positional Arguments:
def person_info(name, age):
print(f"Name: {name}, Age: {age}")
person_info("Alice", 25) # Order matters
# Output: Name: Alice, Age: 25
2. Default Arguments:
def greet(name, message="Hello"): # message has default value
print(f"{message}, {name}!")
greet("Bob") # Hello, Bob!
greet("Bob", "Hi") # Hi, Bob!
3. Keyword Arguments:
def display(name, age, city):
print(f"{name}, {age}, {city}")
display(age=30, name="John", city="NYC") # Order doesn't matter
4. Variable Length Arguments:
*Using args (for multiple positional arguments):
def sum_all(*numbers): # Accepts any number of arguments
total = 0
for num in numbers:
total += num
return total
print(sum_all(1, 2, 3, 4, 5)) # Output: 15
**Using kwargs (for multiple keyword arguments):
def student_info(**details): # Accepts any number of key-value pairs
for key, value in details.items():
print(f"{key}: {value}")
student_info(name="Alice", age=20, grade="A")
# Output:
# name: Alice
# age: 20
# grade: A
3. Dictionary, Conditional Statements & Loops
Dictionary:
A dictionary stores data in key-value pairs. Keys must be unique and immutable.
Creating a Dictionary:
# Method 1: Using curly braces
student = {"name": "Alice", "age": 20, "grade": "A"}
# Method 2: Using dict() constructor
student = dict(name="Alice", age=20, grade="A")
# Empty dictionary
empty_dict = {}
Dictionary Methods:
student = {"name": "Alice", "age": 20, "grade": "A"}
# Accessing values
print(student["name"]) # Alice
print(student.get("age")) # 20
# Adding/Updating
student["city"] = "NYC" # Add new key-value
student["age"] = 21 # Update existing
# Removing items
student.pop("grade") # Remove specific key
del student["city"] # Remove using del
student.clear() # Remove all items
# Other methods
student = {"name": "Alice", "age": 20}
print(student.keys()) # dict_keys(['name', 'age'])
print(student.values()) # dict_values(['Alice', 20])
print(student.items()) # dict_items([('name', 'Alice'), ('age', 20)])
# Update dictionary
student.update({"grade": "A", "city": "NYC"})
Conditional Statements:
If-Else:
age = 18
if age >= 18:
print("Adult")
else:
print("Minor")
Nested If:
marks = 85
if marks >= 50:
print("Pass")
if marks >= 75:
print("Distinction")
else:
print("First Class")
else:
print("Fail")
Elif (else if):
marks = 85
if marks >= 90:
print("Grade: A+")
elif marks >= 75:
print("Grade: A")
elif marks >= 60:
print("Grade: B")
else:
print("Grade: C")
Loops:
For Loop:
# Iterate through a list
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# Using range()
for i in range(5): # 0 to 4
print(i)
# For loop with dictionary
student = {"name": "Alice", "age": 20}
for key, value in student.items():
print(f"{key}: {value}")
For Loop with Else:
for i in range(5):
print(i)
else:
print("Loop completed") # Executes after loop finishes
Break, Continue, Pass:
Break: Exits the loop completely
for i in range(10):
if i == 5:
break # Stop loop when i is 5
print(i)
# Output: 0, 1, 2, 3, 4
Continue: Skips current iteration, continues with next
for i in range(5):
if i == 2:
continue # Skip when i is 2
print(i)
# Output: 0, 1, 3, 4
Pass: Does nothing, placeholder
for i in range(5):
if i == 2:
pass # Does nothing
print(i)
# Output: 0, 1, 2, 3, 4
4. Variable Scope, Modules & OOP Concepts
Local vs Global Variables:
Local Variable: Declared inside a function, accessible only within that function
def my_function():
x = 10 # Local variable
print(x)
my_function() # Output: 10
# print(x) # Error: x is not defined outside function
Global Variable: Declared outside function, accessible everywhere
x = 10 # Global variable
def my_function():
print(x) # Can access global variable
my_function() # Output: 10
print(x) # Output: 10
Modifying Global Variable inside Function:
x = 10
def modify():
global x # Declare x as global
x = 20
modify()
print(x) # Output: 20
Break vs Continue:
| Break | Continue |
| Exits the loop completely | Skips current iteration only |
| Loop terminates | Loop continues with next iteration |
| Used to stop loop when condition met | Used to skip specific iterations |
# Break example
for i in range(10):
if i == 5:
break
print(i) # Output: 0, 1, 2, 3, 4
# Continue example
for i in range(5):
if i == 2:
continue
print(i) # Output: 0, 1, 3, 4
Module vs Package:
| Module | Package |
| A single Python file (.py) | A collection of modules |
| Contains functions, classes, variables | Contains multiple modules in a directory |
| Example: math.py | Example: numpy, pandas |
Using a Module:
# math is a built-in module
import math
print(math.sqrt(16)) # Output: 4.0
# Import specific function
from math import sqrt
print(sqrt(25)) # Output: 5.0
Creating Your Own Module:
# File: mymodule.py
def greet(name):
return f"Hello, {name}!"
# Using the module
import mymodule
print(mymodule.greet("Alice"))
Method Overloading vs Method Overriding:
| Method Overloading | Method Overriding |
| Multiple methods with same name but different parameters | Child class redefines parent class method |
| Python doesn't support true overloading | Python supports overriding |
| Uses default arguments as workaround | Uses inheritance |
Method Overriding Example:
class Animal:
def sound(self):
print("Animal makes sound")
class Dog(Animal):
def sound(self): # Overriding parent method
print("Dog barks")
d = Dog()
d.sound() # Output: Dog barks
Method Overloading Workaround:
class Calculator:
def add(self, a, b=0, c=0): # Using default arguments
return a + b + c
calc = Calculator()
print(calc.add(5)) # Output: 5
print(calc.add(5, 3)) # Output: 8
print(calc.add(5, 3, 2)) # Output: 10
5. Programming Concepts Comparison
Iteration vs Recursion:
| Iteration | Recursion |
| Repeats using loops | Function calls itself |
| Uses for/while loops | Uses function calls |
| Generally faster | Can be slower due to function calls |
| More memory efficient | Uses more memory (call stack) |
Iteration Example:
def factorial_iterative(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
print(factorial_iterative(5)) # Output: 120
Recursion Example:
def factorial_recursive(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial_recursive(n - 1)
print(factorial_recursive(5)) # Output: 120
Call by Value vs Call by Reference:
Python uses "Call by Object Reference" (similar to call by reference for mutable objects)
# Immutable object (behaves like call by value)
def modify_number(x):
x = x + 10
print("Inside function:", x)
num = 5
modify_number(num)
print("Outside function:", num)
# Output:
# Inside function: 15
# Outside function: 5 (unchanged)
# Mutable object (behaves like call by reference)
def modify_list(lst):
lst.append(4)
print("Inside function:", lst)
my_list = [1, 2, 3]
modify_list(my_list)
print("Outside function:", my_list)
# Output:
# Inside function: [1, 2, 3, 4]
# Outside function: [1, 2, 3, 4] (changed)
List vs Tuple:
| List | Tuple |
| Mutable (can be changed) | Immutable (cannot be changed) |
| Uses square brackets [ ] | Uses parentheses ( ) |
| Slower | Faster |
| More memory | Less memory |
| Methods: append, remove, etc. | Limited methods |
# List
my_list = [1, 2, 3]
my_list[0] = 10 # Allowed
my_list.append(4) # Allowed
print(my_list) # Output: [10, 2, 3, 4]
# Tuple
my_tuple = (1, 2, 3)
# my_tuple[0] = 10 # Error: Cannot modify
print(my_tuple) # Output: (1, 2, 3)
Abstract Class vs Interface:
| Abstract Class | Interface |
| Can have abstract and concrete methods | All methods are abstract (in pure interface) |
| Can have instance variables | Cannot have instance variables (in pure interface) |
| Uses ABC module | Python doesn't have true interfaces |
| Single inheritance | Multiple inheritance possible |
Abstract Class Example:
from abc import ABC, abstractmethod
class Vehicle(ABC): # Abstract class
def __init__(self, brand):
self.brand = brand # Concrete attribute
@abstractmethod
def start(self): # Abstract method
pass
def display(self): # Concrete method
print(f"Brand: {self.brand}")
class Car(Vehicle):
def start(self): # Must implement abstract method
print("Car started")
c = Car("Toyota")
c.start() # Car started
c.display() # Brand: Toyota
Interface-like Structure (using ABC):
from abc import ABC, abstractmethod
class Printable(ABC): # Interface-like
@abstractmethod
def print_document(self):
pass
class Scannable(ABC): # Interface-like
@abstractmethod
def scan_document(self):
pass
class Printer(Printable, Scannable): # Multiple inheritance
def print_document(self):
print("Printing...")
def scan_document(self):
print("Scanning...")
Mutable vs Immutable Data Types:
| Mutable | Immutable |
| Can be changed after creation | Cannot be changed after creation |
| list, dict, set | int, float, str, tuple, frozenset |
| Modified in place | Creates new object |
# Mutable (List)
my_list = [1, 2, 3]
my_list[0] = 10 # Changes original list
print(my_list) # [10, 2, 3]
# Immutable (Tuple)
my_tuple = (1, 2, 3)
# my_tuple[0] = 10 # Error
# Immutable (String)
text = "Hello"
# text[0] = 'h' # Error
new_text = text.replace('H', 'h') # Creates new string
print(text) # Hello (unchanged)
print(new_text) # hello (new string)
6. Short Notes on Key Concepts
Data Hiding (Encapsulation):
Restricting access to certain attributes/methods to prevent accidental modification.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute (double underscore)
def deposit(self, amount):
self.__balance += amount
def get_balance(self):
return self.__balance
account = BankAccount(1000)
# print(account.__balance) # Error: Cannot access directly
print(account.get_balance()) # 1000 (Access through method)
Polymorphism:
Same method name behaving differently based on context.
Types:
Compile-time (Method Overloading) - Not directly supported in Python
Runtime (Method Overriding) - Supported
# Runtime Polymorphism
class Shape:
def area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
# Same method name, different behavior
c = Circle(5)
r = Rectangle(4, 6)
print(c.area()) # 78.5
print(r.area()) # 24
Local, Nonlocal, Global Variables:
x = "global" # Global variable
def outer():
x = "enclosing" # Enclosing variable
def inner():
x = "local" # Local variable
print("Local:", x)
inner()
print("Enclosing:", x)
outer()
print("Global:", x)
# Using nonlocal to modify enclosing variable
def outer():
x = "enclosing"
def inner():
nonlocal x # Refers to enclosing scope
x = "modified"
inner()
print(x) # Output: modified
outer()
globals() Function:
Returns a dictionary of global variables.
x = 10
y = 20
print(globals()) # Shows all global variables
print(globals()['x']) # Access specific global variable: 10
Operators in Python:
1. Arithmetic Operators: +, -, , /, //, %, *
print(10 + 3) # 13 (Addition)
print(10 - 3) # 7 (Subtraction)
print(10 * 3) # 30 (Multiplication)
print(10 / 3) # 3.333... (Division)
print(10 // 3) # 3 (Floor division)
print(10 % 3) # 1 (Modulus)
print(10 ** 3) # 1000 (Exponent)
2. Comparison Operators: ==, !=, >, <, >=, <=
print(5 == 5) # True
print(5 != 3) # True
print(5 > 3) # True
3. Logical Operators: and, or, not
print(True and False) # False
print(True or False) # True
print(not True) # False
4. Assignment Operators: =, +=, -=, *=, /=
x = 10
x += 5 # x = x + 5
print(x) # 15
5. Identity Operators: is, is not
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a is c) # True (same object)
print(a is b) # False (different objects)
print(a == b) # True (same value)
6. Membership Operators: in, not in
print(3 in [1, 2, 3]) # True
print('a' in "apple") # True
7. Bitwise Operators: &, |, ^, ~, <<, >>
print(5 & 3) # 1 (AND)
print(5 | 3) # 7 (OR)
print(5 ^ 3) # 6 (XOR)
Operator Precedence & Associativity:
Precedence (Highest to Lowest):
** (Exponent)
*, /, //, % (Multiplication, Division)
+, - (Addition, Subtraction)
<, <=, >, >= (Comparison)
\==, != (Equality)
and, or (Logical)
Associativity:
Most operators: Left to Right
Exponent (**): Right to Left
# Precedence
result = 2 + 3 * 4 # Multiplication first
print(result) # 14 (not 20)
# Associativity
result = 2 ** 3 ** 2 # Right to left
print(result) # 512 (2 ** 9, not 8 ** 2)
Type Conversion:
Implicit Conversion (Automatic):
x = 10 # int
y = 3.5 # float
result = x + y # int converts to float automatically
print(result) # 13.5 (float)
Explicit Conversion (Manual):
# String to int
x = int("10")
print(x) # 10
# Int to string
y = str(100)
print(y) # "100"
# String to float
z = float("3.14")
print(z) # 3.14
# List to tuple
my_list = [1, 2, 3]
my_tuple = tuple(my_list)
print(my_tuple) # (1, 2, 3)
7. Advanced Concepts
Tail Recursion:
Recursion where recursive call is the last operation in the function.
# Non-tail recursion
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1) # Multiplication after recursive call
# Tail recursion (with helper function)
def factorial_tail(n, accumulator=1):
if n == 1:
return accumulator
return factorial_tail(n - 1, n * accumulator) # Recursive call is last
print(factorial(5)) # 120
print(factorial_tail(5)) # 120
self Keyword:
Refers to the current instance of the class. Used to access instance variables and methods.
class Person:
def __init__(self, name, age):
self.name = name # self refers to the current object
self.age = age
def display(self):
print(f"Name: {self.name}, Age: {self.age}")
p1 = Person("Alice", 25)
p2 = Person("Bob", 30)
p1.display() # self = p1
p2.display() # self = p2
Class Variable vs Instance Variable:
class Employee:
company = "ABC Corp" # Class variable (shared by all instances)
def __init__(self, name, salary):
self.name = name # Instance variable (unique to each instance)
self.salary = salary
e1 = Employee("Alice", 50000)
e2 = Employee("Bob", 60000)
print(e1.company) # ABC Corp (same for all)
print(e2.company) # ABC Corp
print(e1.name) # Alice (different for each)
print(e2.name) # Bob
# Changing class variable
Employee.company = "XYZ Corp"
print(e1.company) # XYZ Corp
print(e2.company) # XYZ Corp
super() Keyword:
Calls methods from parent class.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print("Animal speaks")
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call parent constructor
self.breed = breed
def speak(self):
super().speak() # Call parent method
print("Dog barks")
d = Dog("Buddy", "Golden Retriever")
d.speak()
# Output:
# Animal speaks
# Dog barks
Static Method:
Method that doesn't require instance or class. Defined using @staticmethod.
class MathOperations:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Call without creating instance
print(MathOperations.add(5, 3)) # 8
print(MathOperations.multiply(4, 2)) # 8
Tuple Assignment:
Assigning multiple values at once.
# Basic tuple assignment
x, y, z = 10, 20, 30
print(x, y, z) # 10 20 30
# Swapping values
a, b = 5, 10
a, b = b, a # Swap
print(a, b) # 10 5
# Unpacking
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
Identifier & Token:
Identifier: Names given to variables, functions, classes, etc.
# Valid identifiers
name = "Alice"
age2 = 25
_value = 100
# Invalid identifiers
# 2age = 25 # Cannot start with number
# my-name = "Bob" # Cannot use hyphen
Rules for Identifiers:
Must start with letter (a-z, A-Z) or underscore (_)
Can contain letters, digits, underscore
Case sensitive (age and Age are different)
Cannot be a keyword (if, else, for, etc.)
Token: Smallest unit in a program Types:
Keywords (if, else, for, while, etc.)
Identifiers (variable names, function names)
Literals (10, "hello", 3.14)
Operators (+, -, *, /)
Delimiters ((), [], {}, :, ,)
8. Constructors
What is a Constructor?
Special method that initializes an object when it's created. Always named __init__().
class Person:
def __init__(self, name, age): # Constructor
self.name = name
self.age = age
p = Person("Alice", 25) # Constructor called automatically
Features of Constructor:
Automatic Invocation: Called automatically when object is created
Name is Fixed: Always
__init__()Initialize Attributes: Sets initial values for object attributes
No Return Value: Cannot return any value
Can be Overloaded: Using default arguments
Types of Constructors:
1. Default Constructor: Constructor with no parameters (except self).
class Student:
def __init__(self): # Default constructor
self.name = "Unknown"
self.age = 0
print("Default constructor called")
s = Student()
print(s.name) # Unknown
print(s.age) # 0
2. Parameterized Constructor: Constructor that accepts parameters to initialize object with specific values.
class Student:
def __init__(self, name, age, grade): # Parameterized constructor
self.name = name
self.age = age
self.grade = grade
print("Parameterized constructor called")
def display(self):
print(f"Name: {self.name}, Age: {self.age}, Grade: {self.grade}")
# Creating objects with different values
s1 = Student("Alice", 20, "A")
s2 = Student("Bob", 22, "B")
s1.display() # Name: Alice, Age: 20, Grade: A
s2.display() # Name: Bob, Age: 22, Grade: B
Parameterized Constructor with Default Values:
class Product:
def __init__(self, name, price=0, quantity=1):
self.name = name
self.price = price
self.quantity = quantity
def display(self):
print(f"{self.name}: ${self.price} x {self.quantity}")
p1 = Product("Laptop", 1000, 2)
p2 = Product("Mouse") # Uses default price and quantity
p1.display() # Laptop: $1000 x 2
p2.display() # Mouse: $0 x 1
append() vs extend():
| append() | extend() |
| Adds single element | Adds multiple elements |
| Adds element as-is | Iterates and adds each element |
| Increases length by 1 | Increases length by number of elements added |
# append()
list1 = [1, 2, 3]
list1.append(4)
print(list1) # [1, 2, 3, 4]
list1.append([5, 6]) # Adds entire list as single element
print(list1) # [1, 2, 3, 4, [5, 6]]
# extend()
list2 = [1, 2, 3]
list2.extend([4, 5]) # Adds each element individually
print(list2) # [1, 2, 3, 4, 5]
list2.extend("abc") # Extends with each character
print(list2) # [1, 2, 3, 4, 5, 'a', 'b', 'c']
9. Runtime and Compile-time Polymorphism in OOP
Compile-time Polymorphism (Method Overloading):
Method name is same but parameters differ. Decision made at compile time.
Note: Python doesn't support true method overloading, but we can simulate it using default arguments or variable-length arguments.
class Calculator:
# Using default arguments
def add(self, a, b=0, c=0):
return a + b + c
calc = Calculator()
print(calc.add(5)) # 5 (compile-time: uses defaults)
print(calc.add(5, 3)) # 8
print(calc.add(5, 3, 2)) # 10
# Using *args (variable-length arguments)
class MathOps:
def multiply(self, *args):
result = 1
for num in args:
result *= num
return result
m = MathOps()
print(m.multiply(2)) # 2
print(m.multiply(2, 3)) # 6
print(m.multiply(2, 3, 4)) # 24
Runtime Polymorphism (Method Overriding):
Child class provides specific implementation of parent class method. Decision made at runtime based on object type.
class Animal:
def sound(self):
print("Animal makes a sound")
def move(self):
print("Animal moves")
class Dog(Animal):
def sound(self): # Override parent method
print("Dog barks: Woof Woof!")
class Cat(Animal):
def sound(self): # Override parent method
print("Cat meows: Meow Meow!")
class Bird(Animal):
def sound(self): # Override parent method
print("Bird chirps: Chirp Chirp!")
def move(self): # Override parent method
print("Bird flies")
# Runtime polymorphism in action
def make_sound(animal):
animal.sound() # Which sound? Decided at runtime!
# Create objects
dog = Dog()
cat = Cat()
bird = Bird()
# Same method call, different behavior (decided at runtime)
make_sound(dog) # Dog barks: Woof Woof!
make_sound(cat) # Cat meows: Meow Meow!
make_sound(bird) # Bird chirps: Chirp Chirp!
# Another example
animals = [Dog(), Cat(), Bird()]
for animal in animals:
animal.sound() # Runtime decision based on object type
animal.move()
Practical Example with Shapes:
class Shape:
def area(self):
return 0
def perimeter(self):
return 0
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Override
return 3.14 * self.radius ** 2
def perimeter(self): # Override
return 2 * 3.14 * self.radius
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self): # Override
return self.length * self.width
def perimeter(self): # Override
return 2 * (self.length + self.width)
class Triangle(Shape):
def __init__(self, base, height, side1, side2, side3):
self.base = base
self.height = height
self.side1 = side1
self.side2 = side2
self.side3 = side3
def area(self): # Override
return 0.5 * self.base * self.height
def perimeter(self): # Override
return self.side1 + self.side2 + self.side3
# Runtime polymorphism
def display_shape_info(shape):
print(f"Area: {shape.area()}")
print(f"Perimeter: {shape.perimeter()}")
print()
# Create different shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(4, 3, 3, 4, 5)
# Same function, different behavior based on object type
display_shape_info(circle)
display_shape_info(rectangle)
display_shape_info(triangle)
Key Differences:
| Compile-time Polymorphism | Runtime Polymorphism |
| Method overloading | Method overriding |
| Same class | Parent-child relationship |
| Resolved during compilation | Resolved during execution |
| Faster | Slightly slower |
| Not truly supported in Python | Fully supported in Python |
10. Loops, Tuples, Lists with Examples
For Loop:
Used to iterate over a sequence (list, tuple, string, etc.)
# Example 1: Iterating through a list
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(f"I like {fruit}")
# Output:
# I like apple
# I like banana
# I like cherry
# Example 2: Using range()
print("Numbers from 0 to 4:")
for i in range(5):
print(i, end=" ")
print() # 0 1 2 3 4
# Example 3: Range with start and end
print("Numbers from 2 to 7:")
for i in range(2, 8):
print(i, end=" ")
print() # 2 3 4 5 6 7
# Example 4: Range with step
print("Even numbers from 0 to 10:")
for i in range(0, 11, 2):
print(i, end=" ")
print() # 0 2 4 6 8 10
# Example 5: Iterating through string
word = "Python"
for letter in word:
print(letter, end="-")
print() # P-y-t-h-o-n-
While Loop:
Repeats as long as condition is true.
# Example 1: Basic while loop
count = 1
print("Counting from 1 to 5:")
while count <= 5:
print(count, end=" ")
count += 1
print() # 1 2 3 4 5
# Example 2: Sum of numbers
total = 0
number = 1
while number <= 10:
total += number
number += 1
print(f"Sum of 1 to 10: {total}") # 55
# Example 3: User input (infinite loop with break)
# while True:
# password = input("Enter password: ")
# if password == "secret":
# print("Access granted!")
# break
# else:
# print("Wrong password, try again!")
# Example 4: While loop with else
counter = 0
while counter < 3:
print(f"Counter: {counter}")
counter += 1
else:
print("Loop completed successfully!")
Nested Loop:
Loop inside another loop.
# Example 1: Multiplication table
print("Multiplication Table:")
for i in range(1, 4): # Outer loop
for j in range(1, 4): # Inner loop
print(f"{i} x {j} = {i*j}", end=" ")
print() # New line after each row
# Output:
# 1 x 1 = 1 1 x 2 = 2 1 x 3 = 3
# 2 x 1 = 2 2 x 2 = 4 2 x 3 = 6
# 3 x 1 = 3 3 x 2 = 6 3 x 3 = 9
# Example 2: Pattern printing
print("\nStar Pattern:")
for i in range(1, 6): # Outer loop (rows)
for j in range(i): # Inner loop (columns)
print("*", end=" ")
print() # New line
# Output:
# *
# * *
# * * *
# * * * *
# * * * * *
# Example 3: Nested list iteration
students = [
["Alice", 20, "A"],
["Bob", 22, "B"],
["Charlie", 21, "A"]
]
for student in students: # Outer loop
print("Student Information:")
for info in student: # Inner loop
print(f" {info}")
print()
Why Tuple is Immutable?
A tuple is designed to be immutable (unchangeable) for several important reasons:
Tuples are immutable because they are designed to be a fixed collection of items that cannot be changed after creation, which helps ensure data integrity and allows for certain optimizations in programming. This immutability distinguishes them from lists, which can be modified. in depth
1. Memory Efficiency:
# Tuple uses less memory than list
import sys
my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)
print(f"List size: {sys.getsizeof(my_list)} bytes") # Larger
print(f"Tuple size: {sys.getsizeof(my_tuple)} bytes") # Smaller
2. Performance:
# Tuples are faster to access and iterate
# Python can optimize tuple operations because it knows they won't change
3. Dictionary Keys:
# Tuples can be dictionary keys (lists cannot)
location = (40.7128, -74.0060) # Tuple of coordinates
city_map = {location: "New York"} # Valid
# my_list = [40.7128, -74.0060]
# city_map = {my_list: "New York"} # Error! Lists are not hashable
4. Data Integrity:
# Tuples protect data from accidental modification
coordinates = (10, 20, 30)
# coordinates[0] = 15 # Error! Cannot modify
# This is useful when you want to ensure data doesn't change
def get_user_info():
return ("Alice", 25, "Engineer") # Returns fixed data
user = get_user_info()
# user[0] = "Bob" # Error! Data is protected
5. Hashable:
# Tuples are hashable, so they can be used in sets
coordinates_set = {(1, 2), (3, 4), (5, 6)} # Valid
# Lists cannot be used in sets
# coord_set = {[1, 2], [3, 4]} # Error!
Why Python Made This Design Choice:
Safety: Prevents accidental changes to important data
Efficiency: Allows Python to optimize storage and access
Predictability: Once created, tuple data never changes
Use as Keys: Enables use in dictionaries and sets
What is a List?
A list is a collection of items that are ordered, changeable (mutable), and allow duplicate values. Think of it like a shopping list where you can add, remove, or change items.
Creating Lists:
# Empty list
empty_list = []
# List with numbers
numbers = [1, 2, 3, 4, 5]
# List with strings
fruits = ["apple", "banana", "cherry"]
# List with mixed data types
mixed = [1, "hello", 3.14, True, [1, 2, 3]]
# Using list() constructor
my_list = list((1, 2, 3)) # Convert tuple to list
Methods of List:
my_list = [1, 2, 3, 4, 5]
# 1. append() - Add single element at end
my_list.append(6)
print(my_list) # [1, 2, 3, 4, 5, 6]
# 2. extend() - Add multiple elements at end
my_list.extend([7, 8, 9])
print(my_list) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 3. insert() - Add element at specific position
my_list.insert(0, 0) # Insert 0 at index 0
print(my_list) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 4. remove() - Remove first occurrence of value
my_list.remove(0)
print(my_list) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 5. pop() - Remove and return element at index (default: last)
last_item = my_list.pop()
print(last_item) # 9
print(my_list) # [1, 2, 3, 4, 5, 6, 7, 8]
item_at_2 = my_list.pop(2) # Remove item at index 2
print(item_at_2) # 3
print(my_list) # [1, 2, 4, 5, 6, 7, 8]
# 6. clear() - Remove all elements
temp_list = [1, 2, 3]
temp_list.clear()
print(temp_list) # []
# 7. index() - Return index of first occurrence
numbers = [10, 20, 30, 40, 30]
print(numbers.index(30)) # 2 (first occurrence)
# 8. count() - Count occurrences of value
print(numbers.count(30)) # 2
# 9. sort() - Sort list in ascending order
unsorted = [5, 2, 8, 1, 9]
unsorted.sort()
print(unsorted) # [1, 2, 5, 8, 9]
unsorted.sort(reverse=True) # Descending order
print(unsorted) # [9, 8, 5, 2, 1]
# 10. reverse() - Reverse the list
my_list = [1, 2, 3, 4, 5]
my_list.reverse()
print(my_list) # [5, 4, 3, 2, 1]
# 11. copy() - Create shallow copy of list
original = [1, 2, 3]
copied = original.copy()
copied.append(4)
print(original) # [1, 2, 3]
print(copied) # [1, 2, 3, 4]
List Traversal:
Traversal means accessing each element of the list one by one.
# Method 1: Using for loop
fruits = ["apple", "banana", "cherry", "date"]
print("Method 1: Simple for loop")
for fruit in fruits:
print(fruit)
# Output:
# apple
# banana
# cherry
# date
# Method 2: Using index
print("\nMethod 2: Using index with range")
for i in range(len(fruits)):
print(f"Index {i}: {fruits[i]}")
# Output:
# Index 0: apple
# Index 1: banana
# Index 2: cherry
# Index 3: date
# Method 3: Using enumerate() - Get index and value
print("\nMethod 3: Using enumerate()")
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: date
# Method 4: Using while loop
print("\nMethod 4: Using while loop")
i = 0
while i < len(fruits):
print(fruits[i])
i += 1
# Method 5: Reverse traversal
print("\nMethod 5: Reverse traversal")
for fruit in reversed(fruits):
print(fruit)
# Output:
# date
# cherry
# banana
# apple
List Slicing:
List slicing in Python allows you to access specific parts of a list using a simple syntax. You can specify a start index, an end index, and an optional step to retrieve elements, such as myList[1:4] to get items from index 1 to 3.
Slicing allows you to extract a portion of a list. Syntax: list[start:end:step]
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Basic slicing
print(numbers[2:6]) # [2, 3, 4, 5] (from index 2 to 5)
print(numbers[:4]) # [0, 1, 2, 3] (from start to index 3)
print(numbers[5:]) # [5, 6, 7, 8, 9] (from index 5 to end)
print(numbers[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] (entire list)
# Slicing with step
print(numbers[::2]) # [0, 2, 4, 6, 8] (every 2nd element)
print(numbers[1::2]) # [1, 3, 5, 7, 9] (every 2nd element starting from index 1)
print(numbers[::3]) # [0, 3, 6, 9] (every 3rd element)
# Negative indexing
print(numbers[-3:]) # [7, 8, 9] (last 3 elements)
print(numbers[:-3]) # [0, 1, 2, 3, 4, 5, 6] (all except last 3)
print(numbers[-5:-2]) # [5, 6, 7] (from -5 to -2)
# Reverse list using slicing
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(numbers[::-2]) # [9, 7, 5, 3, 1] (reverse, every 2nd)
# Practical examples
fruits = ["apple", "banana", "cherry", "date", "elderberry", "fig"]
# Get first 3 fruits
print(fruits[:3]) # ['apple', 'banana', 'cherry']
# Get last 2 fruits
print(fruits[-2:]) # ['elderberry', 'fig']
# Get middle fruits (skip first and last)
print(fruits[1:-1]) # ['banana', 'cherry', 'date', 'elderberry']
# Get every alternate fruit
print(fruits[::2]) # ['apple', 'cherry', 'elderberry']
# Copy a list using slicing
original = [1, 2, 3, 4, 5]
copy = original[:] # Creates a new list
copy.append(6)
print(original) # [1, 2, 3, 4, 5] (unchanged)
print(copy) # [1, 2, 3, 4, 5, 6]
# Replace part of list
numbers = [0, 1, 2, 3, 4, 5]
numbers[1:4] = [10, 20, 30]
print(numbers) # [0, 10, 20, 30, 4, 5]
# Delete part of list
numbers = [0, 1, 2, 3, 4, 5]
del numbers[1:3]
print(numbers) # [0, 3, 4, 5]
Understanding Slicing with Visual Example:
# List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Index: 0 1 2 3 4 5 6 7 8 9
# -Index: -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# numbers[2:7]
# Start at index 2 (value 2), end before index 7 (value 7)
# Result: [2, 3, 4, 5, 6]
# numbers[-4:-1]
# Start at index -4 (value 6), end before index -1 (value 9)
# Result: [6, 7, 8]
# numbers[1:8:2]
# Start at index 1, end before 8, step of 2
# Result: [1, 3, 5, 7]
11. Files and File Functions
What is a File? A file is a named location on disk used to store related information permanently. Files are used to store data persistently beyond program execution.
File Modes:
'r'- Read (default), error if file doesn't exist'w'- Write, creates new file or truncates existing'a'- Append, creates file if doesn't exist'r+'- Read and Write'w+'- Write and Read (truncates)'a+'- Append and Read'rb','wb'- Binary modes'x'- Exclusive creation, fails if file exists
File Functions:
# a) read() - Reads entire file or n bytes
f = open('file.txt', 'r')
content = f.read() # Read entire file
content = f.read(10) # Read first 10 characters
f.close()
# b) write() - Writes string to file
f = open('file.txt', 'w')
f.write("Hello World\n") # Returns number of characters written
f.close()
# c) seek(offset, whence) - Moves file pointer
# whence: 0=beginning, 1=current, 2=end
f = open('file.txt', 'r')
f.seek(5) # Move to 5th byte from beginning
f.seek(0, 0) # Move to start
f.close()
# d) readline() - Reads single line
f = open('file.txt', 'r')
line = f.readline() # Reads first line
line2 = f.readline() # Reads second line
f.close()
# e) writeline() - NOT a standard method (write() is used)
# For single line: use write()
f = open('file.txt', 'w')
f.write("Line 1\n")
f.close()
# f) tell() - Returns current file pointer position
f = open('file.txt', 'r')
print(f.tell()) # 0 (at start)
f.read(5)
print(f.tell()) # 5 (after reading 5 chars)
f.close()
# g) readlines() - Returns list of all lines
f = open('file.txt', 'r')
lines = f.readlines() # ['line1\n', 'line2\n', ...]
f.close()
# h) writelines() - Writes list of strings
f = open('file.txt', 'w')
lines = ['Line 1\n', 'Line 2\n', 'Line 3\n']
f.writelines(lines)
f.close()
12. Inheritance, Classes & Objects
Types of Inheritance:
In Python, there are five main types of inheritance: single inheritance, multiple inheritance, multilevel inheritance, hierarchical inheritance, and hybrid inheritance. Each type defines how classes can inherit properties and methods from other classes, promoting code reuse and organization.
Single Inheritance: A class inherits from a single base class. This is the most common form.
Multiple Inheritance: A class inherits from multiple parent classes.
Multilevel Inheritance: A chain where a class inherits from a class which itself inherits from another class (e.g.,
A->B->C).Hierarchical Inheritance: Multiple child classes inherit from a single parent class.
Hybrid Inheritance: A combination of two or more of the above types.
# 1. Single Inheritance
class Parent:
def parent_method(self):
print("Parent method")
class Child(Parent):
def child_method(self):
print("Child method")
c = Child()
c.parent_method() # Inherited
c.child_method()
# 2. Multiple Inheritance
class Father:
def skills(self):
print("Gardening")
class Mother:
def skills2(self):
print("Cooking")
class Child(Father, Mother):
pass
c = Child()
c.skills()
c.skills2()
# 3. Multilevel Inheritance
class GrandParent:
def gp_method(self):
print("GrandParent")
class Parent(GrandParent):
def p_method(self):
print("Parent")
class Child(Parent):
def c_method(self):
print("Child")
c = Child()
c.gp_method()
# 4. Hierarchical Inheritance
class Parent:
def show(self):
print("Parent")
class Child1(Parent):
pass
class Child2(Parent):
pass
# 5. Hybrid Inheritance (combination of above)
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
Class and Object Creation:
class Student:
# Class variable (shared by all instances)
school = "ABC School"
def __init__(self, name, roll):
# Instance variables (unique to each instance)
self.name = name
self.roll = roll
def display(self):
print(f"Name: {self.name}, Roll: {self.roll}")
print(f"School: {Student.school}")
# Creating objects
s1 = Student("Alice", 101)
s2 = Student("Bob", 102)
s1.display()
s2.display()
# Class variable is same for all
print(s1.school) # ABC School
print(s2.school) # ABC School
# Changing class variable
Student.school = "XYZ School"
print(s1.school) # XYZ School
print(s2.school) # XYZ School
# Instance variable is unique
print(s1.name) # Alice
print(s2.name) # Bob
Class Variable vs Instance Variable:
Class Variable: Shared across all instances, defined outside
__init__, accessed via class nameInstance Variable: Unique to each object, defined inside
__init__withself
13. String Methods, F-Strings, Recursion, Shallow Copy
String Methods:
Category
| Method | Description | |
| Case conversion | upper() | Converts all characters in the string to uppercase. |
lower() | Converts all characters in the string to lowercase. | |
capitalize() | Converts the first character of the string to uppercase and the rest to lowercase. | |
title() | Converts the first character of each word to uppercase. | |
swapcase() | Swaps the case of every character (lowercase becomes uppercase, and vice versa). | |
| Searching | find() | Returns the lowest index where a substring is found, or -1 if not found. |
rfind() | Returns the highest index where a substring is found, searching from the right. | |
index() | Like find(), but raises a ValueError if the substring is not found. | |
count() | Returns the number of times a specified substring occurs in the string. | |
| Checking (Boolean) | startswith() | Returns True if the string begins with the specified prefix. |
endswith() | Returns True if the string ends with the specified suffix. | |
isalpha() | Returns True if all characters in the string are alphabetic and there is at least one character. | |
isdigit() | Returns True if all characters in the string are digits and there is at least one character. | |
isalnum() | Returns True if all characters in the string are alphanumeric (letters or numbers) and there is at least one character. | |
isspace() | Returns True if all characters in the string are whitespace characters and there is at least one character. | |
| Modifying | strip() | Returns a copy of the string with leading and trailing whitespace (or specified characters) removed. |
replace() | Returns a copy of the string with all occurrences of a specified substring replaced by another substring. | |
split() | Splits the string at whitespace (or a specified separator) and returns a list of substrings. | |
join() | Concatenates elements of an iterable (e.g., a list) into a single string using the string itself as the separator. | |
| Alignment | center() | Centers the string within a specified width, padding with a fill character (space by default). |
ljust() | Left-justifies the string within a specified width, padding with a fill character. | |
rjust() | Right-justifies the string within a specified width, padding with a fill character. |
s = "Hello World"
# Case conversion
print(s.upper()) # HELLO WORLD
print(s.lower()) # hello world
print(s.capitalize()) # Hello world
print(s.title()) # Hello World
print(s.swapcase()) # hELLO wORLD
# Search
print(s.find('o')) # 4 (first occurrence)
print(s.rfind('o')) # 7 (last occurrence)
print(s.index('W')) # 6
print(s.count('l')) # 3
# Check
print(s.startswith('He')) # True
print(s.endswith('ld')) # True
print(s.isalpha()) # False (has space)
print(s.isdigit()) # False
print("123".isdigit()) # True
print(s.isalnum()) # False
print(s.isspace()) # False
# Modify
print(s.strip()) # Removes whitespace
print(s.replace('World', 'Python')) # Hello Python
print(s.split()) # ['Hello', 'World']
print('-'.join(['a','b','c'])) # a-b-c
# Alignment
print(s.center(20, '*')) # ****Hello World*****
print(s.ljust(15)) # Left justify
print(s.rjust(15)) # Right justify
F-Strings (Formatted String Literals):
F-strings, or formatted string literals,provide a concise and readable way to embed Python expressions inside string literals for formatting. They are denoted by prepending an f (or F) before the opening quote of the string.
Key Features and Usage:
Syntax: Use
f"string text {expression}".Evaluation: The expressions within the curly braces
{}are evaluated at runtime and the result is inserted into the string.Readability: Generally considered cleaner and faster than older methods like
format()or the%operator
name = "Alice"
age = 25
marks = 95.567
# Basic f-string
print(f"Name: {name}, Age: {age}")
# Expressions inside f-strings
print(f"Next year age: {age + 1}")
# Formatting
print(f"Marks: {marks:.2f}") # 95.57 (2 decimal places)
print(f"Name: {name:>10}") # Right align in 10 spaces
print(f"Name: {name:<10}") # Left align
print(f"Name: {name:^10}") # Center align
# Multiple lines
message = f"""
Student Details:
Name: {name}
Age: {age}
"""
print(message)
Recursion - Advantages and Disadvantages:
Advantages:
Clean and elegant code
Complex problems broken into simpler sub-problems
Easy to write for tree/graph traversals
Reduces code complexity
Disadvantages:
More memory usage (call stack)
Slower than iterative solutions
Risk of stack overflow
Difficult to debug
Not all problems suit recursion
Shallow Copy of Dictionary:
Python, a shallow copy of a dictionary creates a new dictionary object, but the new dictionary references the same inner objects as the original. If the dictionary contains mutable objects (like lists or other dictionaries), changes made to those inner objects in the copy will affect the original, and vice versa
import copy
original = {
'name': 'Alice',
'marks': [90, 85, 95],
'address': {'city': 'Mumbai'}
}
# Shallow copy methods
shallow1 = original.copy()
shallow2 = dict(original)
shallow3 = copy.copy(original)
# Modifying nested object affects both
shallow1['marks'].append(100)
print(original['marks']) # [90, 85, 95, 100] - Changed!
# Modifying top-level doesn't affect original
shallow1['name'] = 'Bob'
print(original['name']) # Alice - Not changed
# Deep copy for complete independence
deep = copy.deepcopy(original)
deep['marks'].append(88)
print(original['marks']) # [90, 85, 95, 100] - Not changed
Shaloow copy vs Deep copy
| Shallow Copy | Deep Copy | |
| 1. Independence (Top Level) | Creates a new object at the top level, independent from the original. | Creates a new object at the top level, independent from the original. |
| 2. Independence (Nested Objects) | Shares references to nested mutable objects with the original. | Recursively creates new, independent copies of all nested objects. |
| 3. Effect of Modification | Modifying a nested mutable object in the copy also modifies it in the original. | Modifying a nested object in the copy has no effect on the original. |
| 4. Method of Creation | Uses methods like .copy(), dict(), or list slicing [:]. | Requires the copy.deepcopy() function from the copy module. |
| 5. Memory/Performance | Generally faster and uses less memory as it avoids creating new copies of all internal elements. | Slower and uses more memory as it duplicates the entire object structure recursively. |
14. Packing, Unpacking, Literals, Pickling
Packing and Unpacking:
Packing
Definition: The process of collecting multiple individual values into a single compound object.
Default Type: Values are typically packed into a tuple implicitly.
Mechanism: Python automatically creates a single iterable (e.g.,
my_var = 1, 2, 3results in a tuple).Function Use: Used in function definitions with
*argsto handle a variable number of positional arguments, and with**kwargsfor keyword arguments.
Unpacking
Definition: The process of extracting elements from an iterable (tuple, list, string, etc.) and assigning them to distinct variables.
Mechanism: Requires the number of variables on the left side of the assignment operator to match the number of elements in the iterable on the right side.
Error Handling: A
ValueErroris raised if the number of variables does not match the number of elements, unless the asterisk operator is used.Extended Unpacking: The asterisk (
*) operator can collect remaining items into a list (e.g.,a, *b, c = my_list). This is also known as "iterable unpacking" or "starring".
# Packing - Multiple values into one variable
def sum_all(*args): # Packing into tuple
return sum(args)
print(sum_all(1, 2, 3, 4)) # 10
def display(**kwargs): # Packing into dictionary
for key, value in kwargs.items():
print(f"{key}: {value}")
display(name="Alice", age=25)
# Unpacking - Extracting values from collection
# Tuple unpacking
coordinates = (10, 20, 30)
x, y, z = coordinates
print(x, y, z) # 10 20 30
# List unpacking
numbers = [1, 2, 3, 4, 5]
a, *middle, b = numbers
print(a) # 1
print(middle) # [2, 3, 4]
print(b) # 5
# Dictionary unpacking
def greet(name, age):
print(f"Hello {name}, you are {age}")
info = {'name': 'Alice', 'age': 25}
greet(**info) # Unpacking dictionary
Literals - Different Types:
1. String Literals
Used for sequences of characters. They can be enclosed in single quotes ('...'), double quotes ("..."), or triple quotes for multi-line strings ("""...""" or '''...''').
- Example:
"Hello",'World','''Multi-line string'''
2. Numeric Literals
Used for numerical values.
Integer Literals: Whole numbers.
Decimal:
10,100Binary:
0b101(prefix0b)Octal:
0o12(prefix0o)Hexadecimal:
0xFace(prefix0x)
Floating-Point Literals: Numbers with a decimal point or an exponent (
eorE).- Example:
3.14,-0.01,1.2e5(1.2 * 10^5)
- Example:
Complex Literals: Numbers with a real and an imaginary part (uses
jorJ).- Example:
3 + 4j,-1j
- Example:
3. Boolean Literals
Represent truth values.
- Keywords:
True,False
4. Collection Literals
Used to create built-in data structures.
List Literals: Ordered collections of items enclosed in square brackets
[].- Example:
['apple', 'banana', 10]
- Example:
Tuple Literals: Ordered, immutable collections of items enclosed in parentheses
().- Example:
(1, 2, 3)
- Example:
Dictionary Literals: Unordered collections of key-value pairs enclosed in curly braces
{}.- Example:
{'name': 'Bob', 'age': 25}
- Example:
Set Literals: Unordered collections of unique items enclosed in curly braces
{}(Note: an empty{}creates a dictionary).- Example:
{10, 20, 30}
- Example:
5. Special Literal
NoneLiteral: A keyword representing the absence of a value or a null value.
For comprehensive details on how Python parses these, you can refer to the official Python documentation on literals.
# Numeric Literals
decimal = 100
binary = 0b1010 # 10
octal = 0o12 # 10
hexadecimal = 0xA # 10
float_num = 3.14
complex_num = 3 + 4j
scientific = 1.5e2 # 150.0
# String Literals
single = 'Hello'
double = "World"
triple = '''Multi
line
string'''
raw = r"C:\new\folder" # Raw string
# Boolean Literals
is_true = True
is_false = False
# Special Literal
nothing = None
# Collection Literals
list_lit = [1, 2, 3]
tuple_lit = (1, 2, 3)
dict_lit = {'key': 'value'}
set_lit = {1, 2, 3}
Pickling and Unpickling:
Pickling serializes Python objects into a byte stream (using
pickle.dump() in write binary mode 'wb'). This saves object states to a file. Unpickling reverses this process (using pickle.load() in read binary mode 'rb'), reconstructing the object from the bytes. Use the copy module for deep copies.
Pickling: Object →right arrow→ Bytes (
pickle.dump,'wb').Unpickling: Bytes →right arrow→ Object (
pickle.load,'rb').Caution: Unpickle only trusted data due to security risks.
import pickle
# Pickling - Serializing object to binary
data = {
'name': 'Alice',
'marks': [90, 85, 95],
'age': 25
}
# Write to file
with open('data.pkl', 'wb') as f:
pickle.dump(data, f)
# Unpickling - Deserializing from binary
with open('data.pkl', 'rb') as f:
loaded_data = pickle.load(f)
print(loaded_data)
# Pickle to string
pickled_string = pickle.dumps(data)
unpickled = pickle.loads(pickled_string)
15. PVM, Memory Management, C vs Python, Lambda, Tuple
Python Virtual Machine (PVM):
The Python Virtual Machine (PVM) is the runtime environment that executes Python bytecode. When a Python program is run, the source code is first compiled into intermediate bytecode (
.pyc files). The PVM then interprets and executes this bytecode instruction by instruction on the host machine. It manages memory and ensures platform independence.
PVM is the runtime engine of Python
Interprets byte code (.pyc files)
Platform-independent execution layer
Part of Python interpreter
Executes compiled byte code line by line
Memory Management in Python:
Python manages memory primarily through three mechanisms:
Private Heap: All Python objects reside in a private heap memory space managed internally by the PVM.
Automatic Management: The programmer does not manually allocate or deallocate memory (unlike languages like C/C++).
Reference Counting: The core mechanism is reference counting. Each object keeps a count of pointers referencing it. When this count reaches zero, the memory is immediately reclaimed.
Garbage Collection (Generational GC): A secondary garbage collector runs periodically to handle reference cycles (objects that reference each other but are no longer reachable by the main program), preventing memory leaks from cycles.
Memory Pools: Python uses memory pools for small, common objects to speed up allocation and deallocation.
# 1. Automatic memory management via garbage collector
# 2. Reference counting mechanism
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # Shows reference count
# 3. Garbage collection for circular references
import gc
gc.collect() # Force garbage collection
# 4. Memory pools for optimization
# 5. Private heap space for Python objects
C vs Python:
| Feature | C | Python |
| Type | Compiled | Interpreted |
| Speed | Fast | Slower |
| Memory | Manual | Automatic |
| Syntax | Complex | Simple |
| Code Length | More | Less |
| Portability | Less | More |
| Usage | System programming | Web, AI, Data Science |
Lambda Function: Lambda functions in Python are small, anonymous functions defined using the
lambda keyword instead of def. They can take any number of arguments but are restricted to a single expression, whose result is implicitly returned. They are typically used for short, throwaway operations where a full function definition would be overkill, often within functions like filter(), map(), or sorted()
# Syntax: lambda arguments: expression
# Simple lambda
square = lambda x: x ** 2
print(square(5)) # 25
# Multiple arguments
add = lambda x, y: x + y
print(add(3, 4)) # 7
# With built-in functions
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))
print(squares) # [1, 4, 9, 16, 25]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]
# Lambda with sorted
students = [('Alice', 85), ('Bob', 92), ('Charlie', 78)]
sorted_students = sorted(students, key=lambda x: x[1])
print(sorted_students)
Create and Access Tuple: Tuples are created using parentheses
() and hold ordered, immutable collections of items. You access elements using zero-based indexing, just like lists. Trying to modify an element after creation will raise an error.
# Creating tuples
empty = ()
single = (1,) # Comma needed for single element
numbers = (1, 2, 3, 4, 5)
mixed = (1, "Hello", 3.14, True)
nested = ((1, 2), (3, 4))
# Accessing tuple
print(numbers[0]) # 1 (first element)
print(numbers[-1]) # 5 (last element)
print(numbers[1:4]) # (2, 3, 4) (slicing)
# Tuple methods
print(numbers.count(3)) # 1
print(numbers.index(4)) # 3
# Tuple unpacking
a, b, c = (1, 2, 3)
# Immutable
# numbers[0] = 10 # Error!
# But can contain mutable objects
t = ([1, 2], [3, 4])
t[0].append(3) # Works!
print(t) # ([1, 2, 3], [3, 4])
16. Multiple Returns, Bytecode, Unicode, For vs While
Return Multiple Values: In Python, functions implicitly pack multiple returned values into a single tuple, which can then be easily unpacked by the calling code. This is a common and straightforward mechanism.
Returning Multiple Values (Packing)
When you use a single return statement followed by several comma-separated expressions, Python packs those expressions into a tuple automatically.
def calculate(a, b):
sum_val = a + b
diff = a - b
prod = a * b
quot = a / b
return sum_val, diff, prod, quot # Returns tuple
# Unpacking returned values
s, d, p, q = calculate(10, 2)
print(f"Sum: {s}, Diff: {d}, Prod: {p}, Quot: {q}")
# Or as single tuple
result = calculate(10, 2)
print(result) # (12, 8, 20, 5.0)
# Return list
def get_stats():
return [10, 20, 30]
# Return dictionary
def get_info():
return {'name': 'Alice', 'age': 25}
Byte Code and Execution Steps:
Bytecode Explained
Bytecode is a low-level, platform-independent set of instructions that the PVM understands. It is not machine code (which the CPU runs directly) but rather an optimized, intermediate representation of your source code. Bytecode files typically have the .pyc extension and are often stored in __pycache__ directories.
Python Code Execution Steps
The entire execution process can be broken down into these primary steps:
Source Code (
.pyfile):
The process begins with your human-readable Python code (e.g.,script.py).Compilation (to Bytecode):
When you run a program, Python’s compiler parses the source code, checks for syntax errors, and translates it into bytecode instructions. This step happens automatically and is often very quick.Storage (Optional):
For optimization, if the file hasn't changed since the last run, Python saves the generated bytecode to a.pycfile in the__pycache__directory to skip the compilation step next time.Execution (by the PVM):
The Python Virtual Machine (PVM) is the runtime environment that reads and interprets the bytecode instructions one by one. The PVM acts as a mediator between the generic bytecode and the specific operating system/hardware your program is running on.Memory Management and Output:
During execution, the PVM manages the program's memory (using reference counting and the garbage collector) and interacts with the operating system to perform tasks like input/output operations, ultimately producing the program's final results.
This compilation to bytecode and execution via the PVM is why Python is considered an interpreted language and is highly portable across different operating systems.
# Byte code is platform-independent intermediate code
# Steps of execution:
# 1. Source Code (.py file)
# 2. Compiler converts to Byte Code (.pyc in __pycache__)
# 3. PVM (Python Virtual Machine) executes byte code
# 4. Output
# View byte code
import dis
def add(a, b):
return a + b
dis.dis(add) # Disassemble function to see byte code
# Compilation happens automatically
# .pyc files stored in __pycache__ folder
Unicode Value of String:
In Python, every character in a string corresponds to a specific Unicode value (code point). You can easily retrieve and convert between characters and their integer Unicode values using the built-in functions:
ord(): Takes a single character string as input and returns its integer Unicode value.chr(): Takes an integer Unicode value as input and returns the corresponding single-character string.
# Get Unicode (ord function)
print(ord('A')) # 65
print(ord('a')) # 97
print(ord('0')) # 48
print(ord('★')) # 9733
# Get character from Unicode (chr function)
print(chr(65)) # A
print(chr(97)) # a
print(chr(9733)) # ★
# Unicode for entire string
text = "Hello"
for char in text:
print(f"{char}: {ord(char)}")
# Unicode representation
print('\u0041') # A (Unicode escape)
print('\u2605') # ★
For vs While Loop:
For Loop
A for loop is used to iterate over a sequence (like a list, tuple, dictionary, string, or range) or any other iterable object.
Iteration over Iterables: It's designed to automatically handle looping through a predefined set of items one by one.
Known Iterations: Best used when you know in advance exactly how many times you want to loop or when you need to process every item in a collection.
Syntax Simplicity: The syntax is clean and concise for iterating through existing data structures.
Automatic Termination: The loop naturally terminates once all items in the sequence have been processed.
Example Use Case: Printing every item in a shopping list or performing an action a specific number of times using range().
While Loop
A while loop is used to execute a block of code repeatedly as long as a specified condition remains true.
Condition-Based: It focuses purely on a Boolean condition (True/False).
Unknown Iterations: Best used when the number of iterations is unknown and depends on a condition being met during runtime (e.g., waiting for user input, reading from a file until the end is reached).
Manual Control: Requires manual management of loop variables (initializing a counter before the loop and incrementing it inside the loop) to prevent infinite loops.
Potential Infinite Loops: If the condition never becomes
False, the loop runs indefinitely.
Example Use Case: Validating user input until a correct value is entered, or monitoring a changing sensor value.
# FOR Loop
# - Used when iterations are known
# - Iterates over sequence
# - More concise for collections
for i in range(5):
print(i)
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
print(fruit)
# WHILE Loop
# - Used when iterations unknown
# - Continues until condition false
# - Better for indefinite loops
count = 0
while count < 5:
print(count)
count += 1
# While for user input
while True:
ans = input("Continue? (y/n): ")
if ans == 'n':
break
17. 2D List, Keywords, Variables & Scope
2D List in Python:A 2D list (or nested list) in Python is simply
a list where each element is another list. This structure is commonly used to represent data grids, matrices, or tables, where you need both rows and columns.
# Creating 2D list
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Accessing elements
print(matrix[0][0]) # 1
print(matrix[1][2]) # 6
print(matrix[2][1]) # 8
# Using nested loops
for row in matrix:
for element in row:
print(element, end=' ')
print()
# With indices
for i in range(len(matrix)):
for j in range(len(matrix[i])):
print(f"matrix[{i}][{j}] = {matrix[i][j]}")
# List comprehension for 2D list
matrix2 = [[i*j for j in range(3)] for i in range(3)]
print(matrix2)
# Initialize 2D list with zeros
rows, cols = 3, 4
zeros = [[0 for _ in range(cols)] for _ in range(rows)]
Keywords in Python:In Python, keywords are reserved words that have specific meanings to the interpreter and cannot be used as variable names, function names, or any other identifiers.
Here are all the keywords in Python (as of Python 3.10+):
| Keywords |
| Description | |
False | Boolean value (false) |
None | Represents the absence of a value or a null value |
True | Boolean value (true) |
and | Logical operator |
as | Used to create an alias (e.g., in import or with statements) |
assert | Used for debugging; checks if a condition is true |
async | Declares an asynchronous function or context manager |
await | Pauses execution of an async function until a result is returned |
break | Exits the current loop immediately |
class | Defines a class (a blueprint for objects) |
continue | Skips the rest of the current loop iteration and moves to the next one |
def | Defines a function or method |
del | Deletes objects (variables, list items, etc.) |
elif | Short for "else if," used in conditional statements |
else | Executes a block of code if conditions above it are not met |
except | Catches exceptions in a try...except block |
finally | Executes a block of code regardless of whether an exception occurred |
for | Creates a loop to iterate over an iterable object |
from | Used to import specific parts of a module |
global | Declares a variable as global in scope |
if | Starts a conditional statement |
import | Imports modules or packages |
in | Checks if a value is present in a sequence (membership operator) |
is | Tests if two variables refer to the same object (identity operator) |
lambda | Creates a small, anonymous function |
nonlocal | Declares a variable as non-local (in a nested function scope) |
not | Logical operator (negation) |
or | Logical operator |
pass | A null operation; nothing happens (a placeholder) |
raise | Raises an exception manually |
return | Exits a function and returns a value |
try | Starts a block of code where exceptions might occur |
while | Creates a loop that runs as long as a condition is true |
with | Used for simplified handling of external resources (like file I/O) |
# Python has 35 keywords (reserved words)
import keyword
print(keyword.kwlist)
# ['False', 'None', 'True', 'and', 'as', 'assert',
# 'async', 'await', 'break', 'class', 'continue',
# 'def', 'del', 'elif', 'else', 'except', 'finally',
# 'for', 'from', 'global', 'if', 'import', 'in',
# 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass',
# 'raise', 'return', 'try', 'while', 'with', 'yield']
# Cannot be used as identifiers
# class = 10 # SyntaxError
Variables and Scope:
Variable scope in Python determines the visibility and accessibility of a variable within the code. Python follows the
LEGB rule, defining four levels of scope:
Local (L): Variables defined inside a function. They exist only while the function executes and are inaccessible outside it.
Enclosing (E): Variables in the scope of an outer function that contains an inner (nested) function. The inner function can access these variables.
Global (G): Variables defined at the top level of a script or module. They are accessible everywhere in the program. The
globalkeyword is needed to modify a global variable from within a function.Built-in (B): Reserved names for predefined Python functions and keywords (e.g.,
print,len,True,None).
Python checks scopes in the order: Local →right arrow→ Enclosing →right arrow→ Global →right arrow→ Built-in.
# Global Scope
global_var = "I'm global"
def outer():
# Enclosing/Nonlocal Scope
enclosing_var = "I'm enclosing"
def inner():
# Local Scope
local_var = "I'm local"
print(global_var) # Can access
print(enclosing_var) # Can access
print(local_var) # Can access
inner()
# print(local_var) # Error - not accessible
outer()
# LEGB Rule: Local -> Enclosing -> Global -> Built-in
# Global keyword
count = 0
def increment():
global count
count += 1
increment()
print(count) # 1
# Nonlocal keyword
def outer():
x = 10
def inner():
nonlocal x
x = 20
inner()
print(x) # 20
outer()
18. Uses of Python, Compiled vs Interpreted, Membership Operator, SOP vs OOP, For Loop Adv/Disadv
Uses of Python:
Web Development (Django, Flask)
Data Science & Analysis (Pandas, NumPy)
Machine Learning & AI (TensorFlow, PyTorch)
Automation & Scripting
Game Development (Pygame)
Desktop Applications (Tkinter, PyQt)
Scientific Computing
Network Programming
IoT Applications
Cybersecurity
Compiled or Interpreted: Python is both compiled and interpreted:
Source code → Compiled to byte code (.pyc)
Byte code → Interpreted by PVM
Called "interpreted" because compilation is automatic and invisible
Membership Operators:
Python's membership operators are used to test whether a specific value or subsequence is present within a container (like a list, tuple, string, set, or dictionary).
inOperator: Evaluates toTrueif the value on the left side is found within the iterable on the right side.not inOperator: Evaluates toTrueif the value on the left side is not found within the iterable on the right side.
For dictionaries, these operators check for the presence of a key, not a value. These operators provide a highly readable way to perform inclusion checks.
# 'in' and 'not in' operators
# With lists
numbers = [1, 2, 3, 4, 5]
print(3 in numbers) # True
print(10 in numbers) # False
print(10 not in numbers) # True
# With strings
text = "Hello World"
print('H' in text) # True
print('xyz' in text) # False
print('Hello' in text) # True
# With dictionaries (checks keys)
person = {'name': 'Alice', 'age': 25}
print('name' in person) # True
print('Alice' in person) # False (not a key)
# With tuples
tup = (1, 2, 3)
print(2 in tup) # True
# With sets
s = {1, 2, 3}
print(2 in s) # True (very fast)
SOP (Structure-Oriented Programming) vs OOP:
| Feature | SOP | OOP |
| Approach | Functions/Procedures | Objects/Classes |
| Data | Separate from functions | Encapsulated in objects |
| Reusability | Limited | High (inheritance) |
| Security | Low | High (encapsulation) |
| Maintainability | Difficult | Easy |
| Example | C | Python, Java, C++ |
| Focus | What to do | What to work with |
# SOP Style
def calculate_area(length, width):
return length * width
area = calculate_area(10, 5)
# OOP Style
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def calculate_area(self):
return self.length * self.width
rect = Rectangle(10, 5)
area = rect.calculate_area()

For Loop - Advantages and Disadvantages:
Advantages:
Simple and readable syntax
Automatic iteration over sequences
No need to manage counter
Works with any iterable
Prevents off-by-one errors
Built-in iteration protocol
Disadvantages:
Less flexible than while loop
Cannot easily modify iteration variable
Harder to implement complex conditions
Cannot go backwards easily (need reversed())
Fixed iteration pattern
19. Programs---
1. Factorial
def factorial(n): if n == 0 or n == 1: return 1 return n * factorial(n - 1)
print("Factorial of 5:", factorial(5)) # 120
2. Check Prime Number
def is_prime(n): if n <= 1: return False if n == 2: return True if n % 2 == 0: return False
for i in range(3, int(n**0.5) + 1, 2): if n % i == 0: return False return True
print("Is 17 prime?", is_prime(17)) # True print("Is 20 prime?", is_prime(20)) # False
3. Fibonacci using Recursion
def fibonacci(n): if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2)
print("First 10 Fibonacci numbers:") for i in range(10): print(fibonacci(i), end=' ') print()
4. GCD of 2 Numbers
def gcd(a, b): if b == 0: return a return gcd(b, a % b)
print("GCD of 48 and 18:", gcd(48, 18)) # 6
5. Power of X^N
def power(x, n): if n == 0: return 1 if n < 0: return 1 / power(x, -n) return x * power(x, n - 1)
print("2^5 =", power(2, 5)) # 32
6. 1 + (1+2) + (1+2+3) + ...
def series_sum(n): total = 0 for i in range(1, n + 1): # Sum from 1 to i for j in range(1, i + 1): total += j return total
7. Sum of Digits
def sum_of_digits(n): if n == 0: return 0 return n % 10 + sum_of_digits(n // 10)
print("Sum of digits of 12345:", sum_of_digits(12345)) # 15
8. Reverse Number
def reverse_number(n): reversed_num = 0 while n > 0: reversed_num = reversed_num * 10 + n % 10 n //= 10 return reversed_num
Recursive version
def reverse_recursive(n, rev=0): if n == 0: return rev return reverse_recursive(n // 10, rev * 10 + n % 10)
print("Reverse of 12345:", reverse_number(12345)) # 54321
9. String Palindrome
def is_palindrome(s): s = s.lower().replace(" ", "") return s == s[::-1]
Recursive version
def is_palindrome_recursive(s): s = s.lower().replace(" ", "") if len(s) <= 1: return True if s[0] != s[-1]: return False return is_palindrome_recursive(s[1:-1])
print("Is 'racecar' palindrome?", is_palindrome("racecar")) # True print("Is 'hello' palindrome?", is_palindrome("hello")) # False
10. Multiplication Table
def multiplication_table(n): print(f"Multiplication Table of {n}:") for i in range(1, 11): print(f"{n} x {i} = {n * i}")
multiplication_table(5)
11. Matrix Multiplication
def matrix_multiply(A, B): rows_A, cols_A = len(A), len(A[0]) rows_B, cols_B = len(B), len(B[0])
if cols_A != rows_B: return None
# Initialize result matrix result = [[0 for _ in range(cols_B)] for _ in range(rows_A)]
for i in range(rows_A): for j in range(cols_B): for k in range(cols_A): result[i][j] += A[i][k] * B[k][j]
return result
A = [[1, 2, 3], [4, 5, 6]]
B = [[7, 8], [9, 10], [11, 12]]
result = matrix_multiply(A, B) print("\nMatrix Multiplication Result:") for row in result: print(row)
12. File Copy
def copy_file(source, destination): try: with open(source, 'r') as src: content = src.read()
with open(destination, 'w') as dest: dest.write(content)
print(f"File copied from {source} to {destination}") except FileNotFoundError: print(f"Source file {source} not found") except Exception as e: print(f"Error: {e}")
Example usage (commented out):
copy_file('source.txt', 'destination.txt')
13. sin(x) = x - x^3/3! + x^5/5! - ...
import math
def factorial_iter(n): result = 1 for i in range(2, n + 1): result *= i return result
def sin_series(x, terms=10): x_rad = math.radians(x) # Convert to radians result = 0 for n in range(terms): sign = (-1) ** n term = sign * (x_rad ** (2 * n + 1)) / factorial_iter(2 * n + 1) result += term return result
angle = 30 print(f"\nsin({angle}) using series:", sin_series(angle)) print(f"sin({angle}) using math.sin:", math.sin(math.radians(angle)))
14. 1^2 + 2^3 + 3^4 + ...
def power_series(n): total = 0 for i in range(1, n + 1): total += i ** (i + 1) return total
print(f"\n1^2 + 2^3 + 3^4 for n=4:", power_series(4)) # 1 + 8 + 81 = 90
20. Additional Programs---
1. Leap Year Check
def is_leap_year(year): if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): return True return False
year = 2024 if is_leap_year(year): print(f"{year} is a leap year") else: print(f"{year} is not a leap year")
2. Dictionary Creation - Student Info
student = { 'name': 'Alice Johnson', 'roll': 101, 'marks': { 'Math': 95, 'Physics': 88, 'Chemistry': 92 } }
print("\nStudent Information:") print(f"Name: {student['name']}") print(f"Roll: {student['roll']}") print(f"Marks: {student['marks']}")
Multiple students
students = [ {'name': 'Alice', 'roll': 101, 'marks': 95}, {'name': 'Bob', 'roll': 102, 'marks': 88}, {'name': 'Charlie', 'roll': 103, 'marks': 92} ]
print("\nAll Students:") for s in students: print(f"{s['name']} (Roll: {s['roll']}) - Marks: {s['marks']}")
3. Largest Among 3 Numbers
def largest_of_three(a, b, c): if a >= b and a >= c: return a elif b >= a and b >= c: return b else: return c
Alternative using max()
def largest_simple(a, b, c): return max(a, b, c)
num1, num2, num3 = 45, 78, 23 print(f"\nLargest among {num1}, {num2}, {num3}: {largest_of_three(num1, num2, num3)}")
4. Armstrong Number Check
def is_armstrong(n):
# Convert to string to extract digits
num_str = str(n)
num_digits = len(num_str)
# Calculate sum of each digit raised to the power of number of digits
sum_of_powers = sum(int(digit) ** num_digits for digit in num_str)
return sum_of_powers == n
Test Armstrong numbers
test_numbers = [153, 370, 371, 407, 9474, 123] print("\nArmstrong Number Check:") for num in test_numbers: if is_armstrong(num): print(f"{num} is an Armstrong number") else: print(f"{num} is not an Armstrong number")
5. Count Vowels in String
def count_vowels(text): vowels = 'aeiouAEIOU' count = 0 vowel_list = []
for char in text: if char in vowels: count += 1 vowel_list.append(char)
return count, vowel_list
Alternative using list comprehension
def count_vowels_compact(text): vowels = 'aeiouAEIOU' vowel_list = [char for char in text if char in vowels] return len(vowel_list), vowel_list
text = "Hello World, Welcome to Python Programming" count, vowels_found = count_vowels(text) print(f"\nText: {text}") print(f"Number of vowels: {count}") print(f"Vowels found: {vowels_found}")
6. Store Student Info in File and Display
def store_students_to_file(filename, students): try: with open(filename, 'w') as f: f.write("Student Information\n") f.write("=" * 50 + "\n\n")
for student in students: f.write(f"Name: {student['name']}\n") f.write(f"Roll Number: {student['roll']}\n") f.write(f"Marks: {student['marks']}\n") f.write(f"Grade: {student.get('grade', 'N/A')}\n") f.write("-" * 50 + "\n")
print(f"\nStudent information stored in {filename}") except Exception as e: print(f"Error writing to file: {e}")
def display_students_from_file(filename): try: with open(filename, 'r') as f: content = f.read() print("\nDisplaying Student Information from File:") print(content) except FileNotFoundError: print(f"File {filename} not found") except Exception as e: print(f"Error reading file: {e}")
Example data
students_data = [ {'name': 'Alice Johnson', 'roll': 101, 'marks': 95, 'grade': 'A'}, {'name': 'Bob Smith', 'roll': 102, 'marks': 88, 'grade': 'B'}, {'name': 'Charlie Brown', 'roll': 103, 'marks': 92, 'grade': 'A'} ]
Store and display
store_students_to_file('students.txt', students_data) display_students_from_file('students.txt')
7. Bank Account - Class and Object
class BankAccount: # Class variable bank_name = "Python Bank" interest_rate = 3.5
def init(self, account_holder, account_number, balance=0): # Instance variables self.account_holder = account_holder self.account_number = account_number self.balance = balance self.transactions = []
def deposit(self, amount): if amount > 0: self.balance += amount self.transactions.append(f"Deposited: ${amount}") print(f"${amount} deposited successfully") print(f"New balance: ${self.balance}") else: print("Invalid deposit amount")
def withdrawal(self, amount): if amount > 0: if amount <= self.balance: self.balance -= amount self.transactions.append(f"Withdrew: ${amount}") print(f"${amount} withdrawn successfully") print(f"Remaining balance: ${self.balance}") else: print("Insufficient balance!") print(f"Current balance: ${self.balance}") else: print("Invalid withdrawal amount")
def display(self): print("\n" + "=" * 50) print(f"Bank: {BankAccount.bank_name}") print(f"Account Holder: {self.account_holder}") print(f"Account Number: {self.account_number}") print(f"Current Balance: ${self.balance}") print(f"Interest Rate: {BankAccount.interest_rate}%")
if self.transactions: print("\nTransaction History:") for transaction in self.transactions: print(f" - {transaction}") print("=" * 50)
def calculate_interest(self, years): interest = self.balance (BankAccount.interest_rate / 100) years return interest
def check_balance(self): return self.balance
Creating bank account objects
print("\n" + "=" 50) print("BANK ACCOUNT MANAGEMENT SYSTEM") print("=" 50)
Account 1
account1 = BankAccount("Alice Johnson", "ACC001", 1000) account1.display()
print("\n--- Performing Transactions on Account 1 ---") account1.deposit(500) account1.withdrawal(200) account1.withdrawal(2000) # Should fail account1.display()
print(f"\nInterest for 2 years: ${account1.calculate_interest(2):.2f}")
Account 2
print("\n\n--- Creating Second Account ---") account2 = BankAccount("Bob Smith", "ACC002", 5000) account2.display()
print("\n--- Performing Transactions on Account 2 ---") account2.deposit(1000) account2.withdrawal(500) account2.display()
Demonstrate class variable
print(f"\n--- Bank Information (Class Variables) ---") print(f"Bank Name: {BankAccount.bank_name}") print(f"Interest Rate: {BankAccount.interest_rate}%")
Check multiple account balances
print("\n--- Summary of All Accounts ---") print(f"Account 1 ({account1.account_holder}): ${account1.check_balance()}") print(f"Account 2 ({account2.account_holder}): ${account2.check_balance()}")