Documentation

Complete guide to Python programming from basics to advanced

Module 1: Introduction to Python
What is Python?

Python is a high-level, interpreted programming language created by Guido van Rossum and first released in 1991. It is known for its simple, readable syntax and powerful capabilities, making it popular for web development, data science, artificial intelligence, automation, and more.

Key Features of Python
  • Easy to Learn: Simple syntax similar to English
  • Interpreted: Code is executed line by line
  • Dynamically Typed: No need to declare variable types
  • Object-Oriented: Supports OOP concepts
  • Cross-Platform: Runs on Windows, macOS, Linux, etc.
  • Large Standard Library: Rich set of modules and functions
  • Extensive Community: Vast ecosystem of third-party packages
Python Installation

# Check if Python is installed
python --version
# or on some systems
python3 --version

# Install Python on Ubuntu/Debian
sudo apt update
sudo apt install python3

# Install Python on macOS (using Homebrew)
brew install python3

# Install Python on Windows
# Download installer from python.org
Your First Python Program

# hello.py
# This is a comment - ignored by the interpreter

# The print function displays output
print("Hello, World!")
print("Welcome to Python Programming!")

# Using variables
name = "Python"
version = 3.9
print(f"Hello, {name} version {version}!")

# Run the program
python hello.py
# or on some systems
python3 hello.py
Python Interactive Mode

# Start the Python interpreter
python
# or
python3

# Exit the interpreter
exit()
# or press Ctrl+D (Unix) or Ctrl+Z (Windows)
Module 2: Data Types and Variables
Variables in Python

In Python, variables are created when you assign a value to them. Unlike other languages, you don't need to declare the type of a variable.


# Variable assignment
name = "John"  # String
age = 25       # Integer
height = 5.9   # Float
is_student = True  # Boolean

# Multiple assignment
x, y, z = 10, 20, 30
print(x, y, z)  # Output: 10 20 30

# Same value to multiple variables
a = b = c = 0
print(a, b, c)  # Output: 0 0 0
Basic Data Types

# Numeric types
integer = 42                # Integer
floating_point = 3.14       # Float
complex_num = 2 + 3j        # Complex

# Text type
string = "Hello, Python"    # String
multiline = """This is a
multiline string"""         # Multiline string

# Boolean type
is_true = True
is_false = False

# Sequence types
list_example = [1, 2, 3, 4, 5]     # List (mutable)
tuple_example = (1, 2, 3, 4, 5)    # Tuple (immutable)

# Mapping type
dictionary = {"name": "John", "age": 25}  # Dictionary

# Set types
set_example = {1, 2, 3, 4, 5}      # Set (unordered, no duplicates)
frozenset_example = frozenset({1, 2, 3})  # Frozen set (immutable)

# None type
none_value = None
Type Conversion

# Implicit type conversion
integer = 10
floating = 3.14
result = integer + floating  # Result is 13.14 (float)

# Explicit type conversion
str_num = "123"
int_num = int(str_num)      # Convert string to integer
float_num = float(str_num)  # Convert string to float

num_to_str = str(42)        # Convert number to string
int_to_float = float(42)    # Convert integer to float
float_to_int = int(3.99)    # Convert float to integer (truncates)

# Type checking
x = 10
print(type(x))              # 
print(isinstance(x, int))   # True
String Operations

# String concatenation
first_name = "John"
last_name = "Doe"
full_name = first_name + " " + last_name  # "John Doe"

# String formatting
name = "Alice"
age = 30
# Using f-strings (Python 3.6+)
message = f"My name is {name} and I'm {age} years old."
# Using format() method
message = "My name is {} and I'm {} years old.".format(name, age)

# String methods
text = "Hello, World!"
print(text.upper())         # "HELLO, WORLD!"
print(text.lower())         # "hello, world!"
print(text.replace("World", "Python"))  # "Hello, Python!"
print(text.split(","))      # ["Hello", " World!"]
print(text.strip())         # Remove whitespace from both ends

# String indexing and slicing
text = "Python Programming"
print(text[0])              # "P" (first character)
print(text[-1])             # "g" (last character)
print(text[0:6])            # "Python" (slicing)
print(text[7:])             # "Programming" (slicing)
Sets

Sets are unordered collections of unique elements. They are mutable, meaning you can add or remove elements after creation.

Creating Sets

# Creating a set
empty_set = set()  # Empty set
numbers = {1, 2, 3, 4, 5}  # Set with elements
letters = set("hello")  # Set from string (unique characters)
numbers_from_list = set([1, 2, 2, 3, 4, 4, 5])  # Set from list (duplicates removed)

print(empty_set)  # set()
print(numbers)  # {1, 2, 3, 4, 5}
print(letters)  # {'h', 'e', 'l', 'o'}
print(numbers_from_list)  # {1, 2, 3, 4, 5}
Set Methods
Method Description Example
add() Adds an element to the set numbers.add(6)
update() Adds multiple elements to the set numbers.update([7, 8, 9])
remove() Removes an element (raises error if not found) numbers.remove(1)
discard() Removes an element (no error if not found) numbers.discard(10)
pop() Removes and returns an arbitrary element numbers.pop()
clear() Removes all elements numbers.clear()
union() Returns a new set with elements from both sets set1.union(set2)
intersection() Returns a new set with elements common to both sets set1.intersection(set2)
difference() Returns a new set with elements in first set but not in second set1.difference(set2)
symmetric_difference() Returns a new set with elements in either set but not both set1.symmetric_difference(set2)
issubset() Checks if all elements of set are in another set set1.issubset(set2)
issuperset() Checks if set contains all elements of another set set1.issuperset(set2)
isdisjoint() Checks if sets have no elements in common set1.isdisjoint(set2)
Set Operations

# Set operations
set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}

# Union (elements in either set)
union_set = set1.union(set2)  # or set1 | set2
print(union_set)  # {1, 2, 3, 4, 5, 6}

# Intersection (elements in both sets)
intersection_set = set1.intersection(set2)  # or set1 & set2
print(intersection_set)  # {3, 4}

# Difference (elements in first set but not in second)
difference_set = set1.difference(set2)  # or set1 - set2
print(difference_set)  # {1, 2}

# Symmetric difference (elements in either set but not both)
sym_diff_set = set1.symmetric_difference(set2)  # or set1 ^ set2
print(sym_diff_set)  # {1, 2, 5, 6}

# Set relationships
print(set1.issubset({1, 2, 3, 4, 5}))  # True
print(set1.issuperset({1, 2}))  # True
print(set1.isdisjoint({5, 6, 7}))  # False

# In-place operations
set1.update(set2)  # In-place union
set1.intersection_update(set2)  # In-place intersection
set1.difference_update(set2)  # In-place difference
set1.symmetric_difference_update(set2)  # In-place symmetric difference
Frozenset

A frozenset is an immutable version of a set. Once created, elements cannot be added or removed.


# Creating a frozenset
frozen = frozenset([1, 2, 3, 4, 5])
print(frozen)  # frozenset({1, 2, 3, 4, 5})

# Frozenset operations (same as regular sets)
frozen1 = frozenset([1, 2, 3])
frozen2 = frozenset([3, 4, 5])
print(frozen1.union(frozen2))  # frozenset({1, 2, 3, 4, 5})

# Cannot modify frozenset
# frozen.add(6)  # AttributeError: 'frozenset' object has no attribute 'add'
Tuples

Tuples are ordered, immutable sequences of elements. Once created, elements cannot be modified, added, or removed.

Creating Tuples

# Creating tuples
empty_tuple = ()  # Empty tuple
single_element = (5,)  # Tuple with one element (note the comma)
coordinates = (10, 20)  # Tuple with multiple elements
mixed_tuple = (1, "hello", 3.14, True)  # Tuple with different types
from_list = tuple([1, 2, 3, 4, 5])  # Tuple from list

print(empty_tuple)  # ()
print(single_element)  # (5,)
print(coordinates)  # (10, 20)
print(mixed_tuple)  # (1, 'hello', 3.14, True)
print(from_list)  # (1, 2, 3, 4, 5)
Tuple Methods
Method Description Example
count() Returns the number of times a value appears in the tuple my_tuple.count(5)
index() Returns the index of the first occurrence of a value my_tuple.index(5)
Tuple Operations

# Accessing elements
coordinates = (10, 20)
print(coordinates[0])  # 10 (first element)
print(coordinates[1])  # 20 (second element)
print(coordinates[-1])  # 20 (last element)

# Tuple slicing
numbers = (1, 2, 3, 4, 5)
print(numbers[1:4])  # (2, 3, 4)
print(numbers[:3])  # (1, 2, 3)
print(numbers[2:])  # (3, 4, 5)

# Tuple unpacking
point = (3, 4)
x, y = point  # Unpack tuple into variables
print(x, y)  # 3 4

# Tuple concatenation
tuple1 = (1, 2, 3)
tuple2 = (4, 5, 6)
combined = tuple1 + tuple2
print(combined)  # (1, 2, 3, 4, 5, 6)

# Tuple repetition
repeated = tuple1 * 2
print(repeated)  # (1, 2, 3, 1, 2, 3)

# Tuple methods
numbers = (1, 2, 3, 2, 4, 2)
print(numbers.count(2))  # 3 (2 appears 3 times)
print(numbers.index(3))  # 2 (3 is at index 2)

# Checking membership
print(3 in numbers)  # True
print(6 in numbers)  # False

# Getting length
print(len(numbers))  # 6

# Finding min and max
print(min(numbers))  # 1
print(max(numbers))  # 4

# Converting to other types
list_from_tuple = list(numbers)  # Convert to list
set_from_tuple = set(numbers)  # Convert to set (duplicates removed)
Namedtuples

Namedtuples are tuples with named fields. They provide a way to access elements by name instead of index.


from collections import namedtuple

# Creating a namedtuple type
Point = namedtuple('Point', ['x', 'y'])
Color = namedtuple('Color', 'red green blue')

# Creating instances
p1 = Point(10, 20)
p2 = Point(x=5, y=15)
c1 = Color(255, 0, 0)

# Accessing elements
print(p1.x, p1.y)  # 10 20
print(p2[0], p2[1])  # 5 15 (can still access by index)
print(c1.red, c1.green, c1.blue)  # 255 0 0

# Namedtuple methods
print(p1._asdict())  # {'x': 10, 'y': 20}
p3 = p1._replace(x=15)  # Create new Point with x replaced
print(p3)  # Point(x=15, y=20)
print(p1._fields)  # ('x', 'y')
Dictionaries

Dictionaries are unordered collections of key-value pairs. They are mutable, meaning you can add, remove, or modify elements after creation.

Creating Dictionaries

# Creating dictionaries
empty_dict = {}  # Empty dictionary
person = {"name": "John", "age": 25, "city": "New York"}  # Dictionary with key-value pairs
numbers = {1: "one", 2: "two", 3: "three"}  # Dictionary with numeric keys
mixed_keys = {"name": "Alice", 1: [1, 2, 3], (2, 3): "tuple key"}  # Mixed key types

# Using dict() constructor
dict_from_list = dict([("name", "Bob"), ("age", 30)])  # From list of tuples
dict_from_kwargs = dict(name="Charlie", age=35)  # From keyword arguments

print(empty_dict)  # {}
print(person)  # {'name': 'John', 'age': 25, 'city': 'New York'}
print(numbers)  # {1: 'one', 2: 'two', 3: 'three'}
print(mixed_keys)  # {'name': 'Alice', 1: [1, 2, 3], (2, 3): 'tuple key'}
print(dict_from_list)  # {'name': 'Bob', 'age': 30}
print(dict_from_kwargs)  # {'name': 'Charlie', 'age': 35}
Dictionary Methods
Method Description Example
get() Returns the value for a key, with default if key doesn't exist person.get("name", "Unknown")
keys() Returns a view of all keys person.keys()
values() Returns a view of all values person.values()
items() Returns a view of all key-value pairs person.items()
update() Updates the dictionary with elements from another dictionary person.update({"age": 26})
pop() Removes and returns the value for a key person.pop("age")
popitem() Removes and returns a key-value pair person.popitem()
clear() Removes all elements person.clear()
setdefault() Returns the value for a key, sets default if key doesn't exist person.setdefault("country", "USA")
fromkeys() Creates a new dictionary with keys from a sequence dict.fromkeys(["a", "b", "c"], 0)
copy() Returns a shallow copy of the dictionary person.copy()
Dictionary Operations

# Accessing values
person = {"name": "John", "age": 25, "city": "New York"}
print(person["name"])  # John (direct access)
print(person.get("age"))  # 25 (safe access)
print(person.get("country", "Unknown"))  # Unknown (with default)

# Adding or updating elements
person["email"] = "john@example.com"  # Add new key-value pair
person["age"] = 26  # Update existing value
print(person)  # {'name': 'John', 'age': 26, 'city': 'New York', 'email': 'john@example.com'}

# Removing elements
removed_age = person.pop("age")  # Remove and return value
print(removed_age)  # 26
print(person)  # {'name': 'John', 'city': 'New York', 'email': 'john@example.com'}

removed_item = person.popitem()  # Remove and return last key-value pair
print(removed_item)  # ('email', 'john@example.com')

# Checking if key exists
print("name" in person)  # True
print("age" in person)  # False

# Getting keys, values, and items
print(person.keys())  # dict_keys(['name', 'city'])
print(person.values())  # dict_values(['John', 'New York'])
print(person.items())  # dict_items([('name', 'John'), ('city', 'New York')])

# Iterating through dictionary
for key in person:
    print(key, person[key])  # Print key and value

for key, value in person.items():
    print(f"{key}: {value}")  # Print key and value

# Dictionary comprehension
squares = {x: x**2 for x in range(5)}  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
print(squares)

# Merging dictionaries
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
merged = {**dict1, **dict2}  # Python 3.5+
print(merged)  # {'a': 1, 'b': 2, 'c': 3, 'd': 4}

# Dictionary methods
person = {"name": "John", "age": 25}
person.update({"age": 26, "city": "New York"})  # Update with another dictionary
print(person)  # {'name': 'John', 'age': 26, 'city': 'New York'}

# Set default value
country = person.setdefault("country", "USA")  # Returns "USA" and adds to dict
print(person)  # {'name': 'John', 'age': 26, 'city': 'New York', 'country': 'USA'}

# Create dictionary from keys
keys = ["a", "b", "c"]
default_dict = dict.fromkeys(keys, 0)  # {'a': 0, 'b': 0, 'c': 0}
print(default_dict)

# Copy dictionary
person_copy = person.copy()  # Shallow copy
person_deep_copy = person.deepcopy()  # Deep copy (requires import copy)
Dictionary Keys

Dictionary keys must be immutable types (strings, numbers, tuples). Lists and dictionaries cannot be used as keys.


# Valid keys
valid_dict = {
    "string": "value",  # String key
    42: "answer",       # Integer key
    (1, 2): "coordinates",  # Tuple key
    3.14: "pi"          # Float key
}

# Invalid keys
# invalid_dict = {
#     [1, 2]: "list key",  # TypeError: unhashable type: 'list'
#     {"a": 1}: "dict key"  # TypeError: unhashable type: 'dict'
# }
OrderedDict

OrderedDict is a dictionary subclass that remembers the order in which items were inserted.


from collections import OrderedDict

# Creating an OrderedDict
ordered = OrderedDict()
ordered["first"] = 1
ordered["second"] = 2
ordered["third"] = 3

print(ordered)  # OrderedDict([('first', 1), ('second', 2), ('third', 3)])

# Move items to end or beginning
ordered.move_to_end("first")  # Move 'first' to the end
ordered.move_to_end("second", last=False)  # Move 'second' to the beginning

# Pop items from specific positions
item = ordered.popitem(last=False)  # Pop first item
print(item)  # ('second', 2)
DefaultDict

DefaultDict is a dictionary subclass that calls a factory function to supply missing values.


from collections import defaultdict

# Creating a defaultdict with list as default factory
dd = defaultdict(list)
dd["fruits"].append("apple")
dd["fruits"].append("banana")
dd["vegetables"].append("carrot")

print(dd)  # defaultdict(, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})
print(dd["meats"])  # [] (empty list, no KeyError)

# Creating a defaultdict with int as default factory
counter = defaultdict(int)
counter["apples"] += 1
counter["oranges"] += 2

print(counter)  # defaultdict(, {'apples': 1, 'oranges': 2})
print(counter["bananas"])  # 0 (default value)
Module 3: Control Structures
Conditional Statements

# If statement
age = 18
if age >= 18:
    print("You are eligible to vote")

# If-else statement
score = 75
if score >= 60:
    print("You passed the exam")
else:
    print("You failed the exam")

# If-elif-else statement
grade = 85
if grade >= 90:
    letter = 'A'
elif grade >= 80:
    letter = 'B'
elif grade >= 70:
    letter = 'C'
elif grade >= 60:
    letter = 'D'
else:
    letter = 'F'
print(f"Your grade is {letter}")

# Nested if statements
age = 20
has_license = True
if age >= 18:
    if has_license:
        print("You can drive a car")
    else:
        print("You need a license to drive")
else:
    print("You are too young to drive")
Loops

# For loop with range
for i in range(5):  # 0, 1, 2, 3, 4
    print(f"Count: {i}")

# For loop with range(start, stop, step)
for i in range(1, 6, 2):  # 1, 3, 5
    print(f"Odd number: {i}")

# For loop with sequence
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(f"I like {fruit}")

# For loop with enumerate
fruits = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
    print(f"{index + 1}. {fruit}")

# While loop
count = 0
while count < 5:
    print(f"Count: {count}")
    count += 1

# Break and continue
for i in range(10):
    if i == 3:
        continue  # Skip iteration when i is 3
    if i == 7:
        break     # Exit loop when i is 7
    print(f"i = {i}")

# Infinite loop with break condition
count = 0
while True:
    print(f"Count: {count}")
    count += 1
    if count >= 5:
        break
Comprehensions

# List comprehension
squares = [x**2 for x in range(10)]
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# List comprehension with condition
even_numbers = [x for x in range(10) if x % 2 == 0]
print(even_numbers)  # [0, 2, 4, 6, 8]

# Dictionary comprehension
square_dict = {x: x**2 for x in range(5)}
print(square_dict)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Set comprehension
unique_chars = {char for char in "hello world"}
print(unique_chars)  # {' ', 'd', 'e', 'h', 'l', 'o', 'r', 'w'}
Module 4: Functions
Defining and Calling Functions

# Simple function
def greet():
    print("Hello, World!")

# Function with parameters
def greet_person(name):
    print(f"Hello, {name}!")

# Function with return value
def add(a, b):
    return a + b

# Function with default parameter
def greet_with_title(name, title="Mr."):
    print(f"Hello, {title} {name}!")

# Calling functions
greet()  # Output: Hello, World!
greet_person("Alice")  # Output: Hello, Alice!
result = add(5, 3)  # result = 8
print(result)  # Output: 8
greet_with_title("Smith")  # Output: Hello, Mr. Smith!
greet_with_title("Johnson", "Dr.")  # Output: Hello, Dr. Johnson!
Function Arguments

# Positional arguments
def describe_person(name, age, job):
    print(f"{name} is {age} years old and works as a {job}.")

describe_person("John", 30, "developer")

# Keyword arguments
describe_person(age=25, name="Alice", job="designer")

# Default arguments
def power(base, exponent=2):
    return base ** exponent

print(power(3))      # 9 (3^2)
print(power(3, 3))   # 27 (3^3)

# Variable number of arguments (*args)
def sum_all(*numbers):
    total = 0
    for num in numbers:
        total += num
    return total

print(sum_all(1, 2, 3, 4, 5))  # 15

# Variable number of keyword arguments (**kwargs)
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="John", age=30, city="New York")
Scope and Lifetime of Variables

# Local variable
def local_variable_example():
    x = 10  # Local variable
    print(f"Inside function: x = {x}")

local_variable_example()
# print(x)  # Error: x is not defined outside the function

# Global variable
x = 5  # Global variable

def global_variable_example():
    print(f"Inside function (before change): x = {x}")
    x = 10  # This creates a new local variable
    print(f"Inside function (after change): x = {x}")

global_variable_example()
print(f"Outside function: x = {x}")  # Still 5

# Using global keyword
x = 5

def modify_global():
    global x  # Use the global variable
    x = 10
    print(f"Inside function: x = {x}")

modify_global()
print(f"Outside function: x = {x}")  # Now 10
Recursive Functions

# Factorial using recursion
def factorial(n):
    # Base case
    if n <= 1:
        return 1
    # Recursive case
    return n * factorial(n - 1)

print(factorial(5))  # 120

# Fibonacci sequence using recursion
def fibonacci(n):
    # Base cases
    if n <= 0:
        return 0
    if n == 1:
        return 1
    # Recursive case
    return fibonacci(n - 1) + fibonacci(n - 2)

for i in range(10):
    print(fibonacci(i), end=" ")  # 0 1 1 2 3 5 8 13 21 34
Lambda Functions

# Lambda function (anonymous function)
square = lambda x: x ** 2
print(square(5))  # 25

# Lambda with multiple arguments
add = lambda x, y: x + y
print(add(3, 4))  # 7

# Using lambda with built-in functions
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x ** 2, numbers))
print(squared_numbers)  # [1, 4, 9, 16, 25]

even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # [2, 4]
Module 5: Object-Oriented Programming Basics
Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code. The main principles of OOP are:

  • Encapsulation: Bundling data and methods that work on the data
  • Inheritance: Creating new classes from existing ones
  • Polymorphism: Using a single interface for different underlying forms
  • Abstraction: Hiding complex implementation details
Classes and Objects

# Class definition
class Car:
    # Class attribute (shared by all instances)
    wheels = 4
    
    # Constructor (initializer)
    def __init__(self, brand, model, year):
        # Instance attributes (unique to each instance)
        self.brand = brand
        self.model = model
        self.year = year
        self.is_running = False
    
    # Instance method
    def start_engine(self):
        self.is_running = True
        print(f"The {self.brand} {self.model}'s engine is now running.")
    
    def stop_engine(self):
        self.is_running = False
        print(f"The {self.brand} {self.model}'s engine is now off.")
    
    def display_info(self):
        status = "running" if self.is_running else "off"
        print(f"{self.year} {self.brand} {self.model} - Engine: {status}")

# Creating objects (instances of the class)
car1 = Car("Toyota", "Camry", 2023)
car2 = Car("Honda", "Civic", 2022)

# Using objects
car1.start_engine()
car1.display_info()

car2.display_info()
car2.start_engine()
car2.display_info()

# Accessing attributes
print(f"Car 1 brand: {car1.brand}")
print(f"Car 2 model: {car2.model}")
print(f"All cars have {Car.wheels} wheels")
Class Methods and Static Methods

class MathUtils:
    # Class method (receives class as first argument)
    @classmethod
    def circle_area(cls, radius):
        return 3.14159 * radius ** 2
    
    # Static method (doesn't receive class or instance)
    @staticmethod
    def add(a, b):
        return a + b

# Calling class method
area = MathUtils.circle_area(5)
print(f"Circle area: {area}")  # 78.53975

# Calling static method
result = MathUtils.add(3, 4)
print(f"3 + 4 = {result}")  # 7
Properties

class Student:
    def __init__(self, name, age):
        self._name = name  # Convention: _ indicates "protected" attribute
        self._age = age
    
    # Getter for name
    @property
    def name(self):
        return self._name
    
    # Setter for name
    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("Name cannot be empty")
        self._name = value
    
    # Getter for age
    @property
    def age(self):
        return self._age
    
    # Setter for age
    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

student = Student("John", 20)
print(student.name)  # John
student.name = "Alice"  # Using setter
print(student.name)  # Alice
Module 6: Classes and Objects
Special Methods (Magic Methods)

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    # String representation (for users)
    def __str__(self):
        return f"'{self.title}' by {self.author}"
    
    # Official representation (for developers)
    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', {self.pages})"
    
    # Equality comparison
    def __eq__(self, other):
        if not isinstance(other, Book):
            return False
        return (self.title == other.title and 
                self.author == other.author and 
                self.pages == other.pages)
    
    # Less than comparison
    def __lt__(self, other):
        return self.pages < other.pages
    
    # Addition operator
    def __add__(self, other):
        if isinstance(other, Book):
            return self.pages + other.pages
        elif isinstance(other, int):
            return self.pages + other
        return NotImplemented

book1 = Book("Python Crash Course", "Eric Matthes", 560)
book2 = Book("Automate the Boring Stuff", "Al Sweigart", 504)

print(str(book1))  # 'Python Crash Course' by Eric Matthes
print(repr(book1))  # Book('Python Crash Course', 'Eric Matthes', 560)
print(book1 == book2)  # False
print(book1 < book2)  # False (560 > 504)
print(book1 + book2)  # 1064
Inheritance

# Parent class (Superclass)
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass
    
    def eat(self):
        print(f"{self.name} is eating.")

# Child class (Subclass)
class Dog(Animal):
    def __init__(self, name, breed):
        # Call parent constructor
        super().__init__(name)
        self.breed = breed
    
    # Override parent method
    def speak(self):
        print(f"{self.name} says: Woof!")
    
    # Additional method
    def fetch(self):
        print(f"{self.name} is fetching the ball.")

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name)
        self.color = color
    
    def speak(self):
        print(f"{self.name} says: Meow!")

# Create objects
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Tabby")

# Use methods
dog.eat()  # Buddy is eating.
dog.speak()  # Buddy says: Woof!
dog.fetch()  # Buddy is fetching the ball.

cat.eat()  # Whiskers is eating.
cat.speak()  # Whiskers says: Meow!
Multiple Inheritance

class Flyable:
    def fly(self):
        print("Flying high in the sky!")

class Swimmable:
    def swim(self):
        print("Swimming in the water!")

class Duck(Flyable, Swimmable):
    def quack(self):
        print("Quack, quack!")

duck = Duck()
duck.fly()  # Flying high in the sky!
duck.swim()  # Swimming in the water!
duck.quack()  # Quack, quack!
Composition

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower
    
    def start(self):
        print(f"Engine with {self.horsepower} horsepower started.")

class Wheel:
    def __init__(self, size):
        self.size = size
    
    def rotate(self):
        print(f"Wheel of size {self.size} is rotating.")

class Car:
    def __init__(self, brand, model, engine_hp, wheel_size):
        self.brand = brand
        self.model = model
        # Composition: Car has an Engine and Wheels
        self.engine = Engine(engine_hp)
        self.wheels = [Wheel(wheel_size) for _ in range(4)]
    
    def start(self):
        print(f"{self.brand} {self.model} is starting.")
        self.engine.start()
        for wheel in self.wheels:
            wheel.rotate()

car = Car("Toyota", "Camry", 200, 18)
car.start()
Module 7: Inheritance and Polymorphism
Method Overriding

class Shape:
    def __init__(self, color):
        self.color = color
    
    def area(self):
        pass
    
    def perimeter(self):
        pass
    
    def display(self):
        print(f"This is a {self.color} shape.")

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height
    
    # Override parent method
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)
    
    # Override parent method
    def display(self):
        super().display()  # Call parent method
        print(f"It's a rectangle with width {self.width} and height {self.height}.")

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius
    
    def display(self):
        super().display()
        print(f"It's a circle with radius {self.radius}.")

# Create objects
rectangle = Rectangle("red", 5, 3)
circle = Circle("blue", 4)

rectangle.display()
print(f"Area: {rectangle.area()}, Perimeter: {rectangle.perimeter()}")

circle.display()
print(f"Area: {circle.area()}, Perimeter: {circle.perimeter()}")
Polymorphism

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

class Duck(Animal):
    def speak(self):
        return "Quack!"

# Polymorphism in action
def make_animal_speak(animal):
    print(animal.speak())

# Create different animal objects
dog = Dog()
cat = Cat()
duck = Duck()

# Same function call, different behavior
make_animal_speak(dog)  # Woof!
make_animal_speak(cat)  # Meow!
make_animal_speak(duck)  # Quack!

# Polymorphism with a list of objects
animals = [Dog(), Cat(), Duck(), Dog()]
for animal in animals:
    print(animal.speak())
Abstract Base Classes (ABC)

from abc import ABC, abstractmethod

# Abstract base class
class Vehicle(ABC):
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    # Abstract method (must be implemented by subclasses)
    @abstractmethod
    def start(self):
        pass
    
    @abstractmethod
    def stop(self):
        pass
    
    # Concrete method (can be used as is or overridden)
    def display_info(self):
        print(f"{self.brand} {self.model}")

# Concrete subclass
class Car(Vehicle):
    def __init__(self, brand, model, num_doors):
        super().__init__(brand, model)
        self.num_doors = num_doors
    
    def start(self):
        print(f"The {self.brand} {self.model} car is starting.")
    
    def stop(self):
        print(f"The {self.brand} {self.model} car is stopping.")

class Motorcycle(Vehicle):
    def __init__(self, brand, model, has_storage):
        super().__init__(brand, model)
        self.has_storage = has_storage
    
    def start(self):
        print(f"The {self.brand} {self.model} motorcycle is starting.")
    
    def stop(self):
        print(f"The {self.brand} {self.model} motorcycle is stopping.")

# Create objects
car = Car("Toyota", "Camry", 4)
motorcycle = Motorcycle("Harley-Davidson", "Street 750", True)

car.display_info()
car.start()
car.stop()

motorcycle.display_info()
motorcycle.start()
motorcycle.stop()

# This would raise an error because Vehicle is abstract
# vehicle = Vehicle("Generic", "Model")  # TypeError: Can't instantiate abstract class
Duck Typing

class Duck:
    def quack(self):
        print("Quack, quack!")

class Person:
    def quack(self):
        print("I'm pretending to be a duck!")

class Dog:
    def bark(self):
        print("Woof, woof!")

# Duck typing: If it walks like a duck and quacks like a duck, it's a duck
def make_it_quack(duck_like_object):
    duck_like_object.quack()

duck = Duck()
person = Person()
dog = Dog()

make_it_quack(duck)    # Quack, quack!
make_it_quack(person)  # I'm pretending to be a duck!
# make_it_quack(dog)   # AttributeError: 'Dog' object has no attribute 'quack'
Module 8: File Handling
Reading and Writing Files

# Writing to a file
with open("example.txt", "w") as file:
    file.write("Hello, World!\n")
    file.write("This is a sample text file.\n")
    file.write("Python makes file handling easy.\n")

# Reading from a file
with open("example.txt", "r") as file:
    content = file.read()
    print(content)

# Reading line by line
with open("example.txt", "r") as file:
    for line in file:
        print(line.strip())  # strip() removes leading/trailing whitespace

# Reading all lines into a list
with open("example.txt", "r") as file:
    lines = file.readlines()
    print(lines)  # List of strings, each ending with \n

# Appending to a file
with open("example.txt", "a") as file:
    file.write("This line is appended.\n")
File Modes
Mode Description
'r' Read (default)
'w' Write (overwrites existing file)
'a' Append (adds to end of file)
'r+' Read and write
'w+' Write and read (overwrites existing file)
'a+' Append and read
'rb' Read in binary mode
'wb' Write in binary mode
Working with CSV Files

import csv

# Writing to a CSV file
with open("students.csv", "w", newline="") as file:
    writer = csv.writer(file)
    writer.writerow(["Name", "Age", "Grade"])
    writer.writerow(["John", 20, "A"])
    writer.writerow(["Alice", 22, "B"])
    writer.writerow(["Bob", 21, "A"])

# Reading from a CSV file
with open("students.csv", "r") as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)

# Using DictReader and DictWriter
with open("students.csv", "r") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(f"{row['Name']} is {row['Age']} years old and got grade {row['Grade']}")
Working with JSON Files

import json

# Writing to a JSON file
data = {
    "name": "John",
    "age": 30,
    "is_student": False,
    "courses": ["Math", "Science", "History"],
    "address": {
        "street": "123 Main St",
        "city": "New York",
        "zipcode": "10001"
    }
}

with open("data.json", "w") as file:
    json.dump(data, file, indent=4)  # indent for pretty printing

# Reading from a JSON file
with open("data.json", "r") as file:
    loaded_data = json.load(file)
    print(loaded_data)
    print(f"Name: {loaded_data['name']}")
    print(f"Courses: {', '.join(loaded_data['courses'])}")
File and Directory Operations

import os
import shutil

# Check if a file exists
if os.path.exists("example.txt"):
    print("File exists")
else:
    print("File does not exist")

# Get file information
file_info = os.stat("example.txt")
print(f"File size: {file_info.st_size} bytes")
print(f"Last modified: {file_info.st_mtime}")

# Create a directory
os.makedirs("new_directory", exist_ok=True)

# List files in a directory
files = os.listdir(".")
print("Files in current directory:")
for file in files:
    print(file)

# Rename a file
os.rename("example.txt", "renamed_example.txt")

# Copy a file
shutil.copy("renamed_example.txt", "copied_example.txt")

# Move a file
shutil.move("copied_example.txt", "new_directory/moved_example.txt")

# Remove a file
os.remove("renamed_example.txt")

# Remove a directory (must be empty)
os.rmdir("new_directory")  # Will fail if not empty
# shutil.rmtree("new_directory")  # Removes directory and all its contents
Module 9: Exception Handling
Basic Exception Handling

# Try-except block
try:
    num = int(input("Enter a number: "))
    result = 10 / num
    print(f"10 divided by {num} is {result}")
except ValueError:
    print("Invalid input. Please enter a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")
except Exception as e:
    print(f"An error occurred: {e}")

# Try-except-else block
try:
    num = int(input("Enter a number: "))
except ValueError:
    print("Invalid input.")
else:
    print(f"You entered: {num}")

# Try-except-finally block
try:
    file = open("nonexistent_file.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found.")
finally:
    # This block always executes, whether an exception occurred or not
    print("This will always execute.")
    if 'file' in locals():
        file.close()
Handling Multiple Exceptions

# Multiple except blocks
try:
    num1 = int(input("Enter first number: "))
    num2 = int(input("Enter second number: "))
    result = num1 / num2
    print(f"Result: {result}")
except ValueError:
    print("Please enter valid numbers.")
except ZeroDivisionError:
    print("Cannot divide by zero.")
except Exception as e:
    print(f"An unexpected error occurred: {e}")

# Handling multiple exceptions in a single block
try:
    # Code that might raise different exceptions
    pass
except (ValueError, TypeError) as e:
    print(f"Error: {e}")
Raising Exceptions

# Raising an exception
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero.")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(f"Error: {e}")

# Raising a custom exception
class InvalidAgeError(Exception):
    def __init__(self, age):
        self.age = age
        super().__init__(f"Invalid age: {age}. Age must be between 0 and 120.")

def validate_age(age):
    if age < 0 or age > 120:
        raise InvalidAgeError(age)
    return True

try:
    validate_age(150)
except InvalidAgeError as e:
    print(e)
Custom Exceptions

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Insufficient funds: Balance is {balance}, but tried to withdraw {amount}")

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return self.balance

try:
    account = BankAccount(100)
    account.withdraw(150)
except InsufficientFundsError as e:
    print(e)
Context Managers (with statement)

# Using with statement for file handling
with open("example.txt", "r") as file:
    content = file.read()
    # File is automatically closed when exiting the with block

# Creating a custom context manager
class Timer:
    def __init__(self):
        self.start_time = None
    
    def __enter__(self):
        import time
        self.start_time = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.time() - self.start_time
        print(f"Elapsed time: {elapsed:.2f} seconds")
        return False  # Don't suppress exceptions

# Using the custom context manager
with Timer():
    import time
    time.sleep(1)
    print("Doing some work...")
Module 10: Final Project
Project Overview

For the final project, you'll create a comprehensive Contact Management System that demonstrates all the concepts learned throughout the course. This project will include:

  • Object-oriented programming with classes and objects
  • Inheritance and polymorphism
  • File handling for data persistence
  • Exception handling for robust error management
  • User interface with proper input validation
Project Structure

# contact_manager.py

import json
import os
from abc import ABC, abstractmethod

# Abstract base class for contact storage
class ContactStorage(ABC):
    @abstractmethod
    def save_contacts(self, contacts):
        pass
    
    @abstractmethod
    def load_contacts(self):
        pass

# File-based storage implementation
class FileStorage(ContactStorage):
    def __init__(self, filename="contacts.json"):
        self.filename = filename
    
    def save_contacts(self, contacts):
        with open(self.filename, "w") as file:
            json.dump(contacts, file, indent=4)
    
    def load_contacts(self):
        if not os.path.exists(self.filename):
            return []
        with open(self.filename, "r") as file:
            return json.load(file)

# Contact class
class Contact:
    def __init__(self, name, phone, email, address=""):
        self.name = name
        self.phone = phone
        self.email = email
        self.address = address
    
    def to_dict(self):
        return {
            "name": self.name,
            "phone": self.phone,
            "email": self.email,
            "address": self.address
        }
    
    @classmethod
    def from_dict(cls, data):
        return cls(
            data["name"],
            data["phone"],
            data["email"],
            data.get("address", "")
        )
    
    def __str__(self):
        return f"{self.name} - {self.phone} - {self.email}"

# Specialized contact classes
class BusinessContact(Contact):
    def __init__(self, name, phone, email, company, position, address=""):
        super().__init__(name, phone, email, address)
        self.company = company
        self.position = position
    
    def to_dict(self):
        data = super().to_dict()
        data["company"] = self.company
        data["position"] = self.position
        data["type"] = "business"
        return data
    
    @classmethod
    def from_dict(cls, data):
        return cls(
            data["name"],
            data["phone"],
            data["email"],
            data["company"],
            data["position"],
            data.get("address", "")
        )
    
    def __str__(self):
        return f"{self.name} ({self.position} at {self.company}) - {self.phone} - {self.email}"

class PersonalContact(Contact):
    def __init__(self, name, phone, email, relationship, birthday="", address=""):
        super().__init__(name, phone, email, address)
        self.relationship = relationship
        self.birthday = birthday
    
    def to_dict(self):
        data = super().to_dict()
        data["relationship"] = self.relationship
        data["birthday"] = self.birthday
        data["type"] = "personal"
        return data
    
    @classmethod
    def from_dict(cls, data):
        return cls(
            data["name"],
            data["phone"],
            data["email"],
            data["relationship"],
            data.get("birthday", ""),
            data.get("address", "")
        )
    
    def __str__(self):
        return f"{self.name} ({self.relationship}) - {self.phone} - {self.email}"

# Contact Manager class
class ContactManager:
    def __init__(self, storage=None):
        self.contacts = []
        self.storage = storage or FileStorage()
        self.load_contacts()
    
    def load_contacts(self):
        try:
            contacts_data = self.storage.load_contacts()
            self.contacts = []
            for data in contacts_data:
                if data.get("type") == "business":
                    self.contacts.append(BusinessContact.from_dict(data))
                elif data.get("type") == "personal":
                    self.contacts.append(PersonalContact.from_dict(data))
                else:
                    self.contacts.append(Contact.from_dict(data))
        except Exception as e:
            print(f"Error loading contacts: {e}")
            self.contacts = []
    
    def save_contacts(self):
        try:
            contacts_data = [contact.to_dict() for contact in self.contacts]
            self.storage.save_contacts(contacts_data)
            return True
        except Exception as e:
            print(f"Error saving contacts: {e}")
            return False
    
    def add_contact(self, contact):
        self.contacts.append(contact)
        return self.save_contacts()
    
    def remove_contact(self, index):
        if 0 <= index < len(self.contacts):
            del self.contacts[index]
            return self.save_contacts()
        return False
    
    def find_contacts(self, query):
        query = query.lower()
        results = []
        for contact in self.contacts:
            if (query in contact.name.lower() or 
                query in contact.phone.lower() or 
                query in contact.email.lower()):
                results.append(contact)
        return results
    
    def list_contacts(self):
        return self.contacts

# User Interface
class ContactManagerUI:
    def __init__(self):
        self.manager = ContactManager()
    
    def display_menu(self):
        print("\n===== Contact Manager =====")
        print("1. List all contacts")
        print("2. Add a new contact")
        print("3. Search contacts")
        print("4. Remove a contact")
        print("5. Exit")
    
    def list_contacts(self):
        contacts = self.manager.list_contacts()
        if not contacts:
            print("No contacts found.")
            return
        
        print("\n===== Contacts =====")
        for i, contact in enumerate(contacts):
            print(f"{i+1}. {contact}")
    
    def add_contact(self):
        print("\n===== Add New Contact =====")
        print("1. Personal Contact")
        print("2. Business Contact")
        
        choice = input("Enter choice (1-2): ")
        
        try:
            name = input("Name: ")
            phone = input("Phone: ")
            email = input("Email: ")
            address = input("Address (optional): ")
            
            if choice == "1":
                relationship = input("Relationship: ")
                birthday = input("Birthday (optional): ")
                contact = PersonalContact(name, phone, email, relationship, birthday, address)
            elif choice == "2":
                company = input("Company: ")
                position = input("Position: ")
                contact = BusinessContact(name, phone, email, company, position, address)
            else:
                print("Invalid choice.")
                return
            
            if self.manager.add_contact(contact):
                print("Contact added successfully!")
            else:
                print("Failed to add contact.")
        except Exception as e:
            print(f"Error adding contact: {e}")
    
    def search_contacts(self):
        query = input("Enter search query: ")
        results = self.manager.find_contacts(query)
        
        if not results:
            print("No contacts found.")
            return
        
        print("\n===== Search Results =====")
        for i, contact in enumerate(results):
            print(f"{i+1}. {contact}")
    
    def remove_contact(self):
        self.list_contacts()
        try:
            index = int(input("Enter contact number to remove: ")) - 1
            if self.manager.remove_contact(index):
                print("Contact removed successfully!")
            else:
                print("Invalid contact number.")
        except ValueError:
            print("Invalid input. Please enter a number.")
        except Exception as e:
            print(f"Error removing contact: {e}")
    
    def run(self):
        while True:
            self.display_menu()
            choice = input("Enter choice (1-5): ")
            
            if choice == "1":
                self.list_contacts()
            elif choice == "2":
                self.add_contact()
            elif choice == "3":
                self.search_contacts()
            elif choice == "4":
                self.remove_contact()
            elif choice == "5":
                print("Goodbye!")
                break
            else:
                print("Invalid choice. Please try again.")

# Main execution
if __name__ == "__main__":
    ui = ContactManagerUI()
    ui.run()
Project Extensions

Once you've implemented the basic Contact Management System, consider these extensions:

  • Add a GUI using tkinter or PyQt
  • Implement contact groups/categories
  • Add import/export functionality for CSV files
  • Create a web interface using Flask or Django
  • Add contact photo support
  • Implement contact synchronization with cloud services