Python Programming Documentation
Complete guide to Python programming from basics to advanced
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)
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)
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'}
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]
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
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()
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'
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
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...")
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