0% found this document useful (0 votes)
2 views391 pages

Python Set and Collection Operations

The document provides an overview of various Python data structures and techniques, including set operations, the collections module, comprehensions, and lambda functions. It includes practical examples for removing duplicates, finding common items, and using specialized containers like Counter and defaultdict. Additionally, it covers comprehensions for lists, dictionaries, and sets, as well as generator expressions for memory-efficient data processing.

Uploaded by

nandithap.24cse
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views391 pages

Python Set and Collection Operations

The document provides an overview of various Python data structures and techniques, including set operations, the collections module, comprehensions, and lambda functions. It includes practical examples for removing duplicates, finding common items, and using specialized containers like Counter and defaultdict. Additionally, it covers comprehensions for lists, dictionaries, and sets, as well as generator expressions for memory-efficient data processing.

Uploaded by

nandithap.24cse
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

difference = a - b # or a.

difference(b)

{1, 2, 3}
symmetric_diff = a ^ b # or a.symmetric_difference(b)

{1, 2, 3, 6, 7, 8}

Subset/superset checking
is_subset = {1, 2}.issubset(a) # True
is_superset = [Link]({1, 2}) # True

Practical example: Remove duplicates


def remove_duplicates(items):
"""Remove duplicates while preserving order"""
seen = set()
result = []
for item in items:
if item not in seen:
[Link](item)
[Link](item)
return result

numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5]
unique = remove_duplicates(numbers)
print(unique) # [1, 2, 3, 4, 5]

Find common and unique items


list1 = ['apple', 'banana', 'cherry', 'date']
list2 = ['banana', 'cherry', 'elderberry', 'fig']

common = set(list1) & set(list2)


print(f"Common: {common}") # {'banana', 'cherry'}

unique_to_list1 = set(list1) - set(list2)


print(f"Unique to list1: {unique_to_list1}") # {'apple', 'date'}

Syntax highlighting has been disabled due to code size.


---

### Collections Module

**Definition:** The `collections` module provides specialized container datatypes that extend built-in types with additional fu

**Why Use It:** Solves common problems efficiently, provides optimized data structures, and simplifies complex operations

**Example:**
```````````python
from collections import Counter, defaultdict, deque, OrderedDict, namedtuple, ChainMap

# Counter - count hashable objects


words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
word_counts = Counter(words)
print(word_counts) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(word_counts.most_common(2)) # [('apple', 3), ('banana', 2)]

# Practical Counter example


def find_anagrams(word, word_list):
"""Find anagrams using Counter"""
word_counter = Counter([Link]())
anagrams = []
for w in word_list:
if Counter([Link]()) == word_counter:
[Link](w)
return anagrams

words = ['listen', 'silent', 'enlist', 'hello', 'world']


print(find_anagrams('listen', words)) # ['listen', 'silent', 'enlist']

# defaultdict - dictionary with default values


from collections import defaultdict

# Group items by length


words = ['apple', 'pie', 'banana', 'cherry', 'date']
by_length = defaultdict(list)
for word in words:
by_length[len(word)].append(word)

print(dict(by_length))
# {5: ['apple'], 3: ['pie'], 6: ['banana', 'cherry'], 4: ['date']}

# Count occurrences without checking if key exists


text = "hello world"
char_count = defaultdict(int)
for char in text:
char_count[char] += 1 # No KeyError!

# deque - double-ended queue (efficient from both ends)


from collections import deque

# Queue operations
queue = deque(['a', 'b', 'c'])
[Link]('d') # Add to right
[Link]('z') # Add to left
right = [Link]() # Remove from right
left = [Link]() # Remove from left

# Practical deque example: Recent history


class RecentHistory:
"""Keep last N items"""
def __init__(self, max_size=5):
[Link] = deque(maxlen=max_size)

def add(self, item):


[Link](item)
# Automatically removes oldest if full

def get_recent(self):
return list([Link])

history = RecentHistory(3)
for i in range(5):
[Link](f"Item {i}")
print(history.get_recent()) # ['Item 2', 'Item 3', 'Item 4']

# OrderedDict - remembers insertion order (less needed in Python 3.7+)


from collections import OrderedDict

ordered = OrderedDict()
ordered['first'] = 1
ordered['second'] = 2
ordered['third'] = 3

# Move to end
ordered.move_to_end('first')
print(list([Link]())) # ['second', 'third', 'first']

# ChainMap - combine multiple dictionaries


from collections import ChainMap
defaults = {'color': 'blue', 'size': 'medium'}
config = {'size': 'large'}
user_settings = {'theme': 'dark'}

# Combine with priority: user_settings > config > defaults


combined = ChainMap(user_settings, config, defaults)
print(combined['size']) # 'large' (from config)
print(combined['color']) # 'blue' (from defaults)
print(combined['theme']) # 'dark' (from user_settings)
```````````

---

## 13. Comprehensions

### List Comprehensions

**Definition:** List comprehensions provide a concise way to create lists by applying an expression to each item in an iterabl

**Why Use It:** More readable than loops, faster execution, Pythonic style, and reduces code verbosity.

**Example:**
```````````python
# Basic list comprehension
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Traditional equivalent
squares = []
for x in range(10):
[Link](x**2)

# With condition (filter)


evens = [x for x in range(20) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# With transformation and condition


even_squares = [x**2 for x in range(10) if x % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]

# Multiple conditions
result = [x for x in range(50) if x % 2 == 0 if x % 5 == 0]
print(result) # [0, 10, 20, 30, 40]

# If-else in comprehension
labels = ['even' if x % 2 == 0 else 'odd' for x in range(10)]
print(labels) # ['even', 'odd', 'even', 'odd', ...]
# Nested list comprehension
matrix = [[i*j for j in range(1, 4)] for i in range(1, 4)]
print(matrix)
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

# Flatten nested list


nested = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Practical example: Process file lines


def read_and_filter(filename):
"""Read file and filter non-empty lines"""
with open(filename) as f:
return [[Link]() for line in f if [Link]()]

# Extract specific data


users = [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Charlie', 'age': 35}
]
names = [user['name'] for user in users if user['age'] >= 30]
print(names) # ['Alice', 'Charlie']

# String manipulation
words = ['hello', 'world', 'python']
upper_words = [[Link]() for word in words]
print(upper_words) # ['HELLO', 'WORLD', 'PYTHON']

# Cartesian product
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
combinations = [(color, size) for color in colors for size in sizes]
print(combinations)
# [('red', 'S'), ('red', 'M'), ('red', 'L'), ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]
```````````

---

### Dictionary Comprehensions

**Definition:** Dictionary comprehensions create dictionaries in a concise way by iterating over an iterable and constructing

**Why Use It:** Creates dictionaries elegantly, filters and transforms data simultaneously, and improves code readability.
**Example:**
```````````python
# Basic dictionary comprehension
squares = {x: x**2 for x in range(6)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# From two lists (zip)


keys = ['name', 'age', 'city']
values = ['Alice', 30, 'NYC']
person = {k: v for k, v in zip(keys, values)}
print(person) # {'name': 'Alice', 'age': 30, 'city': 'NYC'}

# With condition
numbers = {x: x**2 for x in range(10) if x % 2 == 0}
print(numbers) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

# Transform existing dictionary


original = {'a': 1, 'b': 2, 'c': 3}
doubled = {k: v * 2 for k, v in [Link]()}
print(doubled) # {'a': 2, 'b': 4, 'c': 6}

# Filter dictionary
scores = {'Alice': 85, 'Bob': 92, 'Charlie': 78, 'Diana': 95}
high_scores = {name: score for name, score in [Link]() if score >= 90}
print(high_scores) # {'Bob': 92, 'Diana': 95}

# Swap keys and values


original = {'a': 1, 'b': 2, 'c': 3}
swapped = {v: k for k, v in [Link]()}
print(swapped) # {1: 'a', 2: 'b', 3: 'c'}

# Practical example: Count character frequencies


def char_frequency(text):
"""Count character frequencies"""
return {char: [Link](char) for char in set(text)}

text = "hello"
freq = char_frequency(text)
print(freq) # {'h': 1, 'e': 1, 'l': 2, 'o': 1}

# Group by property
words = ['apple', 'banana', 'apricot', 'blueberry', 'cherry']
by_first_letter = {}
for word in words:
by_first_letter.setdefault(word[0], []).append(word)

# More elegant with comprehension + grouping


from itertools import groupby
[Link]() # groupby requires sorted data
grouped = {k: list(g) for k, g in groupby(words, key=lambda x: x[0])}
print(grouped)

# Convert list of tuples to dict


pairs = [('a', 1), ('b', 2), ('c', 3)]
dictionary = {k: v for k, v in pairs}
print(dictionary) # {'a': 1, 'b': 2, 'c': 3}
```````````

---

### Set Comprehensions

**Definition:** Set comprehensions create sets using a syntax similar to list comprehensions, automatically removing duplica

**Why Use It:** Creates unique collections efficiently, combines filtering with transformation, and leverages set performance

**Example:**
```````````python
# Basic set comprehension
squares = {x**2 for x in range(10)}
print(squares) # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}

# With condition
even_squares = {x**2 for x in range(10) if x % 2 == 0}
print(even_squares) # {0, 4, 16, 36, 64}

# Remove duplicates from list


numbers = [1, 2, 2, 3, 3, 3, 4, 4, 5]
unique = {x for x in numbers}
print(unique) # {1, 2, 3, 4, 5}

# Extract unique characters


text = "hello world"
unique_chars = {char for char in text if [Link]()}
print(unique_chars) # {'h', 'e', 'l', 'o', 'w', 'r', 'd'}

# Practical example: Find unique word lengths


sentence = "the quick brown fox jumps over the lazy dog"
word_lengths = {len(word) for word in [Link]()}
print(word_lengths) # {3, 5, 4}

# Extract unique domains from emails


emails = [
'alice@[Link]',
'bob@[Link]',
'charlie@[Link]',
'diana@[Link]'
]
domains = {[Link]('@')[1] for email in emails}
print(domains) # {'[Link]', '[Link]'}

# Set operations with comprehension


list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
common = {x for x in list1 if x in list2}
print(common) # {4, 5}
```````````

---

### Generator Expressions

**Definition:** Generator expressions are similar to list comprehensions but create generators (lazy evaluation) instead of list

**Why Use It:** Memory efficient for large datasets, faster for single-pass operations, and ideal when you don't need all valu

**Example:**
```````````python
# Generator expression
squares_gen = (x**2 for x in range(10))
print(type(squares_gen)) # <class 'generator'>

# Consume generator
for square in squares_gen:
print(square, end=' ')
# Output: 0 1 4 9 16 25 36 49 64 81

# Memory comparison
import sys

# List comprehension - all in memory


list_comp = [x**2 for x in range(10000)]
print(f"List size: {[Link](list_comp)} bytes") # ~87,000 bytes

# Generator expression - minimal memory


gen_exp = (x**2 for x in range(10000))
print(f"Generator size: {[Link](gen_exp)} bytes") # ~200 bytes

# Use with built-in functions


total = sum(x**2 for x in range(100))
maximum = max(x**2 for x in range(100))
minimum = min(x**2 for x in range(100))

# With condition
even_sum = sum(x for x in range(100) if x % 2 == 0)
print(even_sum)

# Practical example: Process large file


def process_large_file(filename):
"""Process file without loading into memory"""
with open(filename) as f:
# Generator expression for each line
return sum(1 for line in f if 'ERROR' in line)

# Chain generator expressions


numbers = range(1000000)
even_numbers = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in even_numbers)
result = sum(x for x in doubled if x < 1000)

# Check if any/all conditions met


numbers = range(100)
has_even = any(x % 2 == 0 for x in numbers) # True
all_positive = all(x >= 0 for x in numbers) # True

# String processing
text = "The quick brown fox jumps over the lazy dog"
word_lengths = (len(word) for word in [Link]())
avg_length = sum(word_lengths) / len([Link]())
print(f"Average word length: {avg_length}")
```````````

---

## 14. Lambda Functions

### Basic Lambda Functions

**Definition:** Lambda functions are small, anonymous functions defined with the `lambda` keyword, limited to a single exp

**Why Use It:** Provides concise syntax for simple functions, useful for short-lived operations, perfect for higher-order func

**Example:**
```````````python
# Basic lambda
add = lambda x, y: x + y
print(add(5, 3)) # Output: 8
# Traditional function equivalent
def add_traditional(x, y):
return x + y

# Single parameter
square = lambda x: x**2
print(square(5)) # Output: 25

# No parameters
get_pi = lambda: 3.14159
print(get_pi()) # Output: 3.14159

# Multiple operations (still single expression)


complex_calc = lambda x, y: (x + y) * 2 - (x - y)
print(complex_calc(10, 5)) # Output: 25

# Conditional in lambda
max_value = lambda x, y: x if x > y else y
print(max_value(10, 20)) # Output: 20

# Practical example: Quick calculations


calculate_discount = lambda price, discount_pct: price * (1 - discount_pct/100)
final_price = calculate_discount(100, 20)
print(f"Price after discount: ${final_price}") # $80.0

# Lambda with default arguments


power = lambda x, exp=2: x ** exp
print(power(5)) # 25 (uses default exp=2)
print(power(5, 3)) # 125
```````````

---

### Lambda with Built-in Functions

**Definition:** Lambda functions are commonly used with built-in functions like `map()`, `filter()`, and `sorted()` for data tra

**Why Use It:** Eliminates need for separate function definitions, makes code more concise, and is idiomatic Python for sim

**Example:**
```````````python
# map() - apply function to all items
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled) # [2, 4, 6, 8, 10]

# Multiple iterables with map


list1 = [1, 2, 3]
list2 = [10, 20, 30]
sums = list(map(lambda x, y: x + y, list1, list2))
print(sums) # [11, 22, 33]

# filter() - keep items that match condition


numbers = range(10)
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [0, 2, 4, 6, 8]

# Filter strings by length


words = ['hi', 'hello', 'hey', 'goodbye']
long_words = list(filter(lambda w: len(w) > 3, words))
print(long_words) # ['hello', 'goodbye']

# sorted() with key function


students = [
{'name': 'Alice', 'grade': 85},
{'name': 'Bob', 'grade': 92},
{'name': 'Charlie', 'grade': 78}
]
sorted_students = sorted(students, key=lambda s: s['grade'], reverse=True)
for student in sorted_students:
print(f"{student['name']}: {student['grade']}")

# Sort tuples by second element


pairs = [(1, 5), (3, 2), (2, 8), (4, 1)]
sorted_pairs = sorted(pairs, key=lambda x: x[1])
print(sorted_pairs) # [(4, 1), (3, 2), (1, 5), (2, 8)]

# Sort strings by length


words = ['python', 'is', 'awesome', 'and', 'fun']
by_length = sorted(words, key=lambda w: len(w))
print(by_length) # ['is', 'and', 'fun', 'python', 'awesome']

# Practical example: Data transformation pipeline


data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Filter evens, square them, sum result


result = sum(map(lambda x: x**2, filter(lambda x: x % 2 == 0, data)))
print(f"Sum of squared evens: {result}") # 220

# max() and min() with key


people = [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Charlie', 'age': 35}
]
oldest = max(people, key=lambda p: p['age'])
youngest = min(people, key=lambda p: p['age'])
print(f"Oldest: {oldest['name']}") # Charlie
print(f"Youngest: {youngest['name']}") # Bob
```````````

---

### Lambda with reduce()

**Definition:** `reduce()` from `functools` applies a function cumulatively to items in an iterable, reducing it to a single valu

**Why Use It:** Performs cumulative operations, aggregates data, and implements custom reduction logic.

**Example:**
```````````python
from functools import reduce

# Sum all numbers


numbers = [1, 2, 3, 4, 5]
total = reduce(lambda x, y: x + y, numbers)
print(total) # 15

# How it works:
# ((((1 + 2) + 3) + 4) + 5) = 15

# Product of all numbers


product = reduce(lambda x, y: x * y, numbers)
print(product) # 120

# Find maximum
maximum = reduce(lambda x, y: x if x > y else y, numbers)
print(maximum) # 5

# With initial value


numbers = [1, 2, 3, 4, 5]
total_with_initial = reduce(lambda x, y: x + y, numbers, 10)
print(total_with_initial) # 25 (10 + 1 + 2 + 3 + 4 + 5)

# Practical example: Concatenate strings


words = ['Python', 'is', 'awesome']
sentence = reduce(lambda x, y: f"{x} {y}", words)
print(sentence) # "Python is awesome"

# Flatten nested lists


nested = [[1, 2], [3, 4], [5, 6]]
flattened = reduce(lambda x, y: x + y, nested)
print(flattened) # [1, 2, 3, 4, 5, 6]

# Complex example: Calculate factorial


def factorial(n):
return reduce(lambda x, y: x * y, range(1, n + 1))

print(factorial(5)) # 120

# Merge dictionaries
dicts = [{'a': 1}, {'b': 2}, {'c': 3}]
merged = reduce(lambda x, y: {**x, **y}, dicts)
print(merged) # {'a': 1, 'b': 2, 'c': 3}
```````````

---

### Lambda in Data Structures

**Definition:** Lambda functions can be stored in data structures like dictionaries and lists to create callable collections.

**Why Use It:** Creates function mappings, implements simple dispatching, and provides flexible callback systems.

**Example:**
```````````python
# Dictionary of operations
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else None
}

# Use operations
result = operations['add'](10, 5)
print(result) # 15

result = operations['multiply'](10, 5)
print(result) # 50

# Calculator using dictionary


def calculator(operation, x, y):
ops = {
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b if b != 0 else 'Error: Division by zero'
}
return [Link](operation, lambda a, b: 'Invalid operation')(x, y)

print(calculator('+', 10, 5)) # 15


print(calculator('*', 10, 5)) # 50
print(calculator('%', 10, 5)) # Invalid operation

# List of validators
validators = [
lambda x: len(x) >= 8,
lambda x: any([Link]() for c in x),
lambda x: any([Link]() for c in x)
]

def validate_password(password):
"""Check if password meets all criteria"""
return all(validator(password) for validator in validators)

print(validate_password("Short1")) # False (too short)


print(validate_password("LongPassword1")) # True

# Event handlers
event_handlers = {
'click': lambda: print("Button clicked!"),
'hover': lambda: print("Mouse over"),
'keypress': lambda: print("Key pressed")
}

def trigger_event(event_name):
handler = event_handlers.get(event_name)
if handler:
handler()

trigger_event('click') # Output: Button clicked!

# Sorting configurations
sort_options = {
'name': lambda item: item['name'],
'age': lambda item: item['age'],
'score': lambda item: item['score']
}

data = [
{'name': 'Alice', 'age': 30, 'score': 85},
{'name': 'Bob', 'age': 25, 'score': 92},
{'name': 'Charlie', 'age': 35, 'score': 78}
]
sorted_by_score = sorted(data, key=sort_options['score'], reverse=True)
for item in sorted_by_score:
print(item['name'], item['score'])
```````````

---

## 15. Built-in Functions

### Numeric Functions

**Definition:** Python provides built-in functions for common mathematical operations without requiring imports.

**Why Use It:** Convenient for basic math operations, optimized for performance, and universally available.

**Example:**
```````````python
# abs() - absolute value
print(abs(-10)) # 10
print(abs(3.14)) # 3.14
print(abs(-5.7)) # 5.7

# round() - round to nearest integer or decimal places


print(round(3.14159)) #3
print(round(3.14159, 2)) # 3.14
print(round(3.5)) # 4 (rounds to nearest even)
print(round(2.5)) # 2 (rounds to nearest even)

# max() - maximum value


print(max(1, 5, 3, 9, 2)) # 9
print(max([10, 20, 5])) # 20

# With key function


words = ['python', 'is', 'awesome']
longest = max(words, key=len)
print(longest) # 'awesome'

# min() - minimum value


print(min(1, 5, 3, 9, 2)) # 1
print(min([10, 20, 5])) #5

# sum() - sum of iterable


numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(total) # 15
# With start value
total_with_start = sum(numbers, 10)
print(total_with_start) # 25

# pow() - power function


print(pow(2, 3)) # 8 (2^3)
print(pow(5, 2)) # 25 (5^2)
print(pow(2, 3, 5)) # 3 (2^3 % 5)

# divmod() - quotient and remainder


quotient, remainder = divmod(17, 5)
print(f"{17} ÷ {5} = {quotient} remainder {remainder}") # 17 ÷ 5 = 3 remainder 2

# Practical example: Calculate average


def calculate_average(numbers):
"""Calculate average of numbers"""
if not numbers:
return 0
return sum(numbers) / len(numbers)

scores = [85, 92, 78, 95, 88]


avg = calculate_average(scores)
print(f"Average score: {avg:.2f}") # 87.60

# Find range (max - min)


numbers = [15, 23, 8, 42, 16]
range_value = max(numbers) - min(numbers)
print(f"Range: {range_value}") # 34
```````````

---

### Type Conversion Functions

**Definition:** Functions that convert values from one data type to another.

**Why Use It:** Essential for data processing, user input handling, and type compatibility.

**Example:**
```````````python
# int() - convert to integer
print(int("123")) # 123
print(int(3.14)) # 3 (truncates)
print(int("FF", 16)) # 255 (hexadecimal)
print(int("1010", 2)) # 10 (binary)

# float() - convert to float


print(float("3.14")) # 3.14
print(float(5)) # 5.0
print(float("inf")) # inf (infinity)

# str() - convert to string


print(str(123)) # "123"
print(str(3.14)) # "3.14"
print(str([1, 2, 3])) # "[1, 2, 3]"

# bool() - convert to boolean


print(bool(1)) # True
print(bool(0)) # False
print(bool("")) # False (empty string)
print(bool("hello")) # True (non-empty string)
print(bool([])) # False (empty list)
print(bool([1, 2])) # True (non-empty list)

# list() - convert to list


print(list("hello")) # ['h', 'e', 'l', 'l', 'o']
print(list((1, 2, 3))) # [1, 2, 3]
print(list(range(5))) # [0, 1, 2, 3, 4]

# tuple() - convert to tuple


print(tuple([1, 2, 3])) # (1, 2, 3)
print(tuple("abc")) # ('a', 'b', 'c')

# set() - convert to set (removes duplicates)


print(set([1, 2, 2, 3, 3, 3])) # {1, 2, 3}
print(set("hello")) # {'h', 'e', 'l', 'o'}

# dict() - convert to dictionary


pairs = [('a', 1), ('b', 2)]
print(dict(pairs)) # {'a': 1, 'b': 2}

# Practical example: Safe input conversion


def get_integer_input(prompt):
"""Get integer input with error handling"""
while True:
try:
return int(input(prompt))
except ValueError:
print("Invalid input. Please enter a number.")

# Parse CSV data


def parse_csv_line(line):
"""Parse CSV line with type conversion"""
name, age@my_decorator
def calculate(x, y):
"""Add two numbers"""
return x + y

result = calculate(5, 3)
print(calculate.__name__) # Output: calculate
print(calculate.__doc__) # Output: Add two numbers
```````````

---

### Class Decorators

**Definition:** Decorators that modify or enhance entire classes, similar to function decorators but operating on class definiti

**Why Use It:** Adds functionality to all class instances, implements singleton patterns, adds automatic registration, or modi

**Example:**
```````````python
# Basic class decorator
def add_greeting(cls):
"""Add greeting method to class"""
[Link] = lambda self: f"Hello from {[Link]}"
return cls

@add_greeting
class Person:
def __init__(self, name):
[Link] = name

person = Person("Alice")
print([Link]()) # Output: Hello from Alice

# Singleton class decorator


def singleton(cls):
"""Ensure only one instance of class exists"""
instances = {}

def get_instance(*args, **kwargs):


if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]

return get_instance

@singleton
class Database:
def __init__(self):
print("Database initialized")
[Link] = "Connected"

db1 = Database() # Output: Database initialized


db2 = Database() # No output (returns same instance)
print(db1 is db2) # Output: True

# Practical example: Auto-registration decorator


REGISTERED_PLUGINS = {}

def register_plugin(name):
"""Register class as a plugin"""
def decorator(cls):
REGISTERED_PLUGINS[name] = cls
return cls
return decorator

@register_plugin("csv_processor")
class CSVProcessor:
def process(self, data):
return f"Processing CSV: {data}"

@register_plugin("json_processor")
class JSONProcessor:
def process(self, data):
return f"Processing JSON: {data}"

# Use registered plugins


processor = REGISTERED_PLUGINS["csv_processor"]()
print([Link]("data")) # Output: Processing CSV: data
```````````

---

### Built-in Decorators

**Definition:** Python provides built-in decorators like `@property`, `@staticmethod`, `@classmethod` for common patterns

**Why Use It:** Leverages Python's standard functionality, makes code more Pythonic, and follows established patterns.

**Example:**
```````````python
# @property decorator
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Get temperature in Celsius"""
return self._celsius

@property
def fahrenheit(self):
"""Get temperature in Fahrenheit"""
return (self._celsius * 9/5) + 32

temp = Temperature(25)
print([Link]) # Output: 25
print([Link]) # Output: 77.0

# @staticmethod and @classmethod


class MathUtils:
PI = 3.14159

@staticmethod
def add(a, b):
"""Static method - no self or cls"""
return a + b

@classmethod
def circle_area(cls, radius):
"""Class method - receives cls"""
return [Link] * radius ** 2

print([Link](5, 3)) # Output: 8


print(MathUtils.circle_area(5)) # Output: 78.53975

# @functools.lru_cache for memoization


from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
"""Cached Fibonacci calculation"""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

# Much faster due to caching


print(fibonacci(100)) # Computes quickly

# Check cache info


print(fibonacci.cache_info())
```````````

---

## 10. Context Managers

### Using Context Managers (with statement)

**Definition:** Context managers handle setup and cleanup of resources automatically using the `with` statement, ensuring p

**Why Use It:** Prevents resource leaks, guarantees cleanup even on errors, makes code cleaner and more reliable.

**Example:**
```````````python
# File handling with context manager
with open('[Link]', 'w') as file:
[Link]("Hello, World!")
# File automatically closed here

# Multiple context managers


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Thread locks with context manager


import threading

lock = [Link]()

def safe_operation():
with lock:
# Critical section - only one thread at a time
print("Performing thread-safe operation")
# Lock automatically released

# Database connection example


class DatabaseConnection:
def __enter__(self):
print("Opening database connection")
return self

def __exit__(self, exc_type, exc_val, exc_tb):


print("Closing database connection")
if exc_type is not None:
print(f"Exception occurred: {exc_val}")
return False # Don't suppress exceptions
with DatabaseConnection() as db:
print("Using database")
# Output:
# Opening database connection
# Using database
# Closing database connection
```````````

---

### Creating Context Managers (Class-based)

**Definition:** Create custom context managers by implementing `__enter__()` and `__exit__()` methods in a class.

**Why Use It:** Encapsulates resource management logic, provides reusable resource handling, and follows Python best prac

**Example:**
```````````python
# File manager with logging
class FileManager:
"""Context manager for file operations"""

def __init__(self, filename, mode):


[Link] = filename
[Link] = mode
[Link] = None

def __enter__(self):
"""Setup - open file"""
print(f"Opening {[Link]}")
[Link] = open([Link], [Link])
return [Link]

def __exit__(self, exc_type, exc_val, exc_tb):


"""Cleanup - close file"""
if [Link]:
print(f"Closing {[Link]}")
[Link]()

if exc_type is not None:


print(f"Exception occurred: {exc_val}")

return False # Don't suppress exceptions

# Using custom context manager


with FileManager('[Link]', 'w') as f:
[Link]("Hello!")
# Output:
# Opening [Link]
# Closing [Link]

# Timer context manager


import time

class Timer:
"""Context manager to measure execution time"""

def __enter__(self):
[Link] = [Link]()
return self

def __exit__(self, exc_type, exc_val, exc_tb):


[Link] = [Link]()
[Link] = [Link] - [Link]
print(f"Elapsed time: {[Link]:.4f} seconds")
return False

# Using timer
with Timer():
# Some time-consuming operation
total = sum(range(1000000))
# Output: Elapsed time: 0.0234 seconds

# Practical example: Temporary directory


import tempfile
import shutil
import os

class TemporaryDirectory:
"""Context manager for temporary directory"""

def __enter__(self):
self.temp_dir = [Link]()
print(f"Created temporary directory: {self.temp_dir}")
return self.temp_dir

def __exit__(self, exc_type, exc_val, exc_tb):


if [Link](self.temp_dir):
[Link](self.temp_dir)
print(f"Removed temporary directory: {self.temp_dir}")
return False

with TemporaryDirectory() as temp_dir:


# Use temporary directory
temp_file = [Link](temp_dir, '[Link]')
with open(temp_file, 'w') as f:
[Link]("Temporary data")
# Directory automatically cleaned up
```````````

---

### Creating Context Managers (Function-based)

**Definition:** Use the `@contextmanager` decorator from `contextlib` to create context managers using generator functions

**Why Use It:** Simpler syntax than class-based approach, perfect for straightforward resource management, more concise c

**Example:**
```````````python
from contextlib import contextmanager

# Basic context manager


@contextmanager
def simple_context():
"""Simple context manager using generator"""
print("Entering context")
yield
print("Exiting context")

with simple_context():
print("Inside context")
# Output:
# Entering context
# Inside context
# Exiting context

# Context manager that yields a value


@contextmanager
def managed_resource():
"""Context manager that provides a resource"""
print("Acquiring resource")
resource = {"data": "important"}
try:
yield resource
finally:
print("Releasing resource")
[Link]()

with managed_resource() as res:


print(f"Using: {res}")
# Output:
# Acquiring resource
# Using: {'data': 'important'}
# Releasing resource

# Practical example: Changing directory


import os

@contextmanager
def change_directory(path):
"""Temporarily change working directory"""
original_dir = [Link]()
try:
[Link](path)
yield
finally:
[Link](original_dir)

print(f"Current: {[Link]()}")
with change_directory('/tmp'):
print(f"Inside with: {[Link]()}")
print(f"After with: {[Link]()}")

# Database transaction example


@contextmanager
def transaction(connection):
"""Database transaction context manager"""
try:
print("BEGIN TRANSACTION")
yield connection
print("COMMIT")
# [Link]()
except Exception as e:
print(f"ROLLBACK due to {e}")
# [Link]()
raise

# Usage
class FakeConnection:
pass

with transaction(FakeConnection()) as conn:


print("Executing queries...")
# If exception occurs, automatically rolls back
```````````

---
### Suppressing Exceptions with Context Managers

**Definition:** The `[Link]` context manager allows you to ignore specific exceptions without try-except blocks

**Why Use It:** Cleaner code for expected exceptions, improves readability, and reduces boilerplate error handling.

**Example:**
```````````python
from contextlib import suppress
import os

# Without suppress
try:
[Link]('file_that_might_not_exist.txt')
except FileNotFoundError:
pass

# With suppress (cleaner)


with suppress(FileNotFoundError):
[Link]('file_that_might_not_exist.txt')

# Suppress multiple exceptions


with suppress(ValueError, TypeError, KeyError):
# Try operations that might fail
value = int("not a number")

# Practical example: Safe dictionary access


config = {'host': 'localhost', 'port': 8080}

# Old way
try:
del config['missing_key']
except KeyError:
pass

# New way
with suppress(KeyError):
del config['missing_key']

print(config) # Still intact

# Cleanup operations example


files_to_delete = ['[Link]', '[Link]', '[Link]']

for filename in files_to_delete:


with suppress(FileNotFoundError, PermissionError):
[Link](filename)
print(f"Attempted to delete {filename}")
```````````

---

## 11. Regular Expressions

### Basic Pattern Matching

**Definition:** Regular expressions (regex) are patterns used to match character combinations in strings, providing powerful

**Why Use It:** Validates input formats, extracts data from text, searches complex patterns, and performs sophisticated text r

**Example:**
```````````python
import re

# Basic search
text = "My phone number is 123-456-7890"
pattern = r'\d{3}-\d{3}-\d{4}' # Pattern for phone number

match = [Link](pattern, text)


if match:
print(f"Found: {[Link]()}") # Output: Found: 123-456-7890

# Find all matches


text = "Emails: john@[Link], jane@[Link], bob@[Link]"
email_pattern = r'[\w.-]+@[\w.-]+\.\w+'

emails = [Link](email_pattern, text)


print(emails) # Output: ['john@[Link]', 'jane@[Link]', 'bob@[Link]']

# Match at beginning
text = "Python is awesome"
if [Link](r'Python', text):
print("Text starts with Python")

# Full match
if [Link](r'\d{3}-\d{4}', '123-4567'):
print("Exact match found")

# Practical example: Validate username


def validate_username(username):
"""Username: 3-16 chars, letters/numbers/underscore"""
pattern = r'^[a-zA-Z0-9_]{3,16}# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+
---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables
**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
``````````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables

print(f"{name} is {age} years old") # Output: Alice is 30 years old


``````````

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
``````````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>
# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
``````````

---

### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
``````````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False

# Type conversion (casting)


str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
``````````

---

## 2. Control Flow

### If-Elif-Else Statements


**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
``````````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
``````````

---

### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
``````````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
``````````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.

**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
``````````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")

# Enumerate - get both index and value


for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
``````````

---

### While Loops


**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
``````````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
``````````

---

### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
``````````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
``````````

---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
``````````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


``````````

---

## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
``````````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"

message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement
print_welcome() # Output: Welcome to Python!
``````````

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
``````````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
``````````

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.
**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
``````````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100

# Practical example: Finding maximum


def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
``````````

---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
``````````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
``````````

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
``````````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
``````````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec

**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
``````````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
``````````

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
``````````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"
def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
``````````

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be
**Example:**
``````````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"

def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)

print(book1) # Output: 'Python Basics' by John Doe


print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
``````````

---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
``````````python
# Parent class
class Animal:
"""Base class for all animals"""

def __init__(self, name, age):


[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
``````````

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function
**Example:**
``````````python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance

@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""
def __init__(self, celsius):
[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
``````````

---

### Properties

**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
``````````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius
@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
``````````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
``````````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from

# Standard library examples


import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")

# Practical example: Using multiple imports


from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
``````````

---

### Creating Your Own Modules


**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


``````````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""

@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
``````````

Use it in another file:


``````````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
``````````

---

### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
``````````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]

def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
``````````

---

## 6. File Handling

### Reading Files

**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
``````````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
``````````

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
``````````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
``````````
---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
``````````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Practical example: Safe file operations


def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
``````````

---
### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
``````````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
``````````

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.
**Example:**
``````````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
``````````

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
``````````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
``````````

---

### Raising Exceptions

**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
``````````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")

invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
``````````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
``````````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass

class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")

if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)
try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
``````````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
``````````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""

def __init__(self, filename, max_lines):


[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
``````````

---

### Generators

**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
``````````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34
# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5

# Practical example: Large file processing


def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
``````````

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
``````````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory

# Generator expression (creates values on demand)


squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
``````````
---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
``````````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []
def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
``````````

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
``````````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
``````````
---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
``````````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
``````````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
``````````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring

# Practical example: Complete decorator template


from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
"""
return bool([Link](pattern, username))

print(validate_username("john_doe")) # True
print(validate_username("ab")) # False (too short)
print(validate_username("user@name")) # False (invalid char)
``````````

---

### Regular Expression Groups

**Definition:** Groups capture parts of matched patterns using parentheses, allowing extraction of specific portions of text.

**Why Use It:** Extracts specific data from matches, creates reusable patterns, and enables complex replacements.

**Example:**
``````````python
import re

# Basic groups
text = "John Smith, Age: 30"
pattern = r'(\w+)\s+(\w+),\s+Age:\s+(\d+)'

match = [Link](pattern, text)


if match:
first_name = [Link](1)
last_name = [Link](2)
age = [Link](3)
print(f"{first_name} {last_name} is {age} years old")

# Named groups (more readable)


pattern = r'(?P<first>\w+)\s+(?P<last>\w+),\s+Age:\s+(?P<age>\d+)'
match = [Link](pattern, text)
if match:
print([Link]('first')) # John
print([Link]('last')) # Smith
print([Link]('age')) # 30

# Extract multiple pieces of information


text = "Date: 2024-03-15, Time: 14:30:00"
pattern = r'Date:\s+(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}),\s+Time:\s+(?P<hour>\d{2}):(?P<minute>\d{2}):(?
match = [Link](pattern, text)
if match:
print([Link]())
# Output: {'year': '2024', 'month': '03', 'day': '15', ...}

# Practical example: Parse log entries


log_entry = "2024-03-15 14:30:00 ERROR Database connection failed"
log_pattern = r'(?P<date>\S+)\s+(?P<time>\S+)\s+(?P<level>\w+)\s+(?P<message>.*)'

match = [Link](log_pattern, log_entry)


if match:
log_data = [Link]()
print(f"Level: {log_data['level']}")
print(f"Message: {log_data['message']}")
``````````

---

### Regular Expression Substitution

**Definition:** Replace matched patterns in strings using `[Link]()`, which can use captured groups in the replacement.

**Why Use It:** Transforms text based on patterns, cleans data, reformats strings, and performs intelligent replacements.

**Example:**
``````````python
import re

# Simple substitution
text = "I love cats and cats are great"
new_text = [Link](r'cats', 'dogs', text)
print(new_text) # Output: I love dogs and dogs are great

# Limit replacements
new_text = [Link](r'cats', 'dogs', text, count=1)
print(new_text) # Output: I love dogs and cats are great

# Using groups in replacement


text = "John Smith (30), Jane Doe (25)"
pattern = r'(\w+)\s+(\w+)\s+\((\d+)\)'
replacement = r'\1 \2 is \3 years old'
new_text = [Link](pattern, replacement, text)
print(new_text) # Output: John Smith is 30 years old, Jane Doe is 25 years old

# Using function for replacement


def uppercase_match(match):
"""Convert matched text to uppercase"""
return [Link](0).upper()

text = "hello world python"


new_text = [Link](r'\b\w+\b', uppercase_match, text)
print(new_text) # Output: HELLO WORLD PYTHON

# Practical example: Clean phone numbers


def clean_phone_number(phone):
"""Remove all non-digit characters"""
return [Link](r'\D', '', phone)

phone = "(123) 456-7890"


clean = clean_phone_number(phone)
print(clean) # Output: 1234567890

# Format dates
text = "Dates: 03/15/2024 and 12/25/2024"
pattern = r'(\d{2})/(\d{2})/(\d{4})'
replacement = r'\3-\1-\2' # Change to YYYY-MM-DD
new_text = [Link](pattern, replacement, text)
print(new_text) # Output: Dates: 2024-03-15 and 2024-12-25

# Redact sensitive information


text = "SSN: 123-45-6789, Credit Card: 1234-5678-9012-3456"
redacted = [Link](r'\d{3}-\d{2}-\d{4}', 'XXX-XX-XXXX', text)
redacted = [Link](r'\d{4}-\d{4}-\d{4}-\d{4}', 'XXXX-XXXX-XXXX-XXXX', redacted)
print(redacted)
``````````

---

### Compiling Regular Expressions

**Definition:** Compile regex patterns into pattern objects for better performance when using the same pattern multiple time

**Why Use It:** Improves performance with repeated use, provides better organization, and enables pattern reuse.

**Example:**
``````````python
import re

# Without compiling (slower for repeated use)


for text in ["test@[Link]", "another@[Link]", "third@[Link]"]:
if [Link](r'[\w.-]+@[\w.-]+\.\w+', text):
print(f"Valid: {text}")

# With compiling (faster for repeated use)


email_pattern = [Link](r'[\w.-]+@[\w.-]+\.\w+')

for text in ["test@[Link]", "another@[Link]", "third@[Link]"]:


if email_pattern.match(text):
print(f"Valid: {text}")

# Compiled pattern with flags


case_insensitive = [Link](r'python', [Link])
print(case_insensitive.search("I love Python")) # Matches
print(case_insensitive.search("I love PYTHON")) # Also matches

# Practical example: Multiple validation patterns


class Validator:
"""Validation using compiled patterns"""

def __init__(self):
self.email_pattern = [Link](r'^[\w.-]+@[\w.-]+\.\w+# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+

---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
`````````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables

print(f"{name} is {age} years old") # Output: Alice is 30 years old


`````````

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
`````````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
`````````

---

### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
`````````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False
# Type conversion (casting)
str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
`````````

---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
`````````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
`````````

---
### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
`````````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
`````````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.

**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
`````````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")
# Enumerate - get both index and value
for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
`````````

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
`````````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
`````````

---
### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
`````````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
`````````

---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
`````````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


`````````

---

## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
`````````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"
message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


`````````

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
`````````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
`````````

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
`````````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100

# Practical example: Finding maximum


def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
`````````
---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
`````````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
`````````

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
`````````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
`````````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec

**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
`````````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
`````````

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
`````````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
`````````

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
`````````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"

def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)
print(book1) # Output: 'Python Basics' by John Doe
print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
`````````

---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
`````````python
# Parent class
class Animal:
"""Base class for all animals"""
def __init__(self, name, age):
[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
`````````

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
`````````python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance

@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
`````````

---

### Properties
**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
`````````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
`````````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
`````````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from

# Standard library examples


import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")
# Practical example: Using multiple imports
from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
`````````

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


`````````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""
@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
`````````

Use it in another file:


`````````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
`````````

---

### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
`````````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]
def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
`````````

---

## 6. File Handling

### Reading Files

**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
`````````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
`````````

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
`````````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
`````````

---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
`````````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())
# Practical example: Safe file operations
def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
`````````

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
`````````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
`````````

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
`````````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
`````````

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
`````````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
`````````

---

### Raising Exceptions


**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
`````````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")
invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
`````````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
`````````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass

class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
`````````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
`````````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""
def __init__(self, filename, max_lines):
[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
`````````

---

### Generators

**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
`````````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5

# Practical example: Large file processing


def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
`````````

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
`````````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory

# Generator expression (creates values on demand)


squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
`````````

---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
`````````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
`````````

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
`````````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
`````````

---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
`````````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
`````````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
`````````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring
# Practical example: Complete decorator template
from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
""")
self.phone_pattern = [Link](r'^\d{3}-\d{3}-\d{4}# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+

---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
````````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables

print(f"{name} is {age} years old") # Output: Alice is 30 years old


````````

---

### Data Types


**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
````````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
````````

---

### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.
**Example:**
````````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False

# Type conversion (casting)


str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
````````

---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
````````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
````````

---

### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
````````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
````````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.

**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
````````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")

# Enumerate - get both index and value


for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
````````

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
````````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
````````

---

### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
````````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
````````

---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case
**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
````````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


````````

---

## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage
**Example:**
````````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"

message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


````````

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
````````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
````````

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
````````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100

# Practical example: Finding maximum


def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
````````

---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
````````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
````````

---

### Function Annotations (Type Hints)


**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
````````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
````````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec

**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
````````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
````````

---

## 4. Object-Oriented Programming
### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
````````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
````````

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
````````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"

def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]
def __eq__(self, other):
"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)

print(book1) # Output: 'Python Basics' by John Doe


print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
````````

---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren
**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
````````python
# Parent class
class Animal:
"""Base class for all animals"""

def __init__(self, name, age):


[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
````````

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
````````python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance

@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])
@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
````````

---

### Properties

**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
````````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius
# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9
thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
````````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
````````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from

# Standard library examples


import random
import os
from pathlib import Path
from collections import Counter, defaultdict
# Using imported modules
random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")

# Practical example: Using multiple imports


from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
````````

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


````````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius
def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""

@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
````````

Use it in another file:


````````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
````````

---

### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
````````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]

def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
````````

---

## 6. File Handling

### Reading Files

**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
````````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
````````

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
````````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")
# Write multiple lines at once
lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
````````

---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
````````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Practical example: Safe file operations


def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
````````

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
````````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
````````

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
````````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
````````

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
````````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
````````

---

### Raising Exceptions

**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
````````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")

invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
````````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
````````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass

class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")

if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
````````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
````````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""

def __init__(self, filename, max_lines):


[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
````````

---

### Generators

**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
````````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5

# Practical example: Large file processing


def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()
# Process file without loading into memory
def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
````````

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
````````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory

# Generator expression (creates values on demand)


squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81
# Generator expression with condition
evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
````````

---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
````````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
````````

---

## 9. Decorators
### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
````````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds
# Practical example: Logging decorator
def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
````````

---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
````````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
````````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
````````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring

# Practical example: Complete decorator template


from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
""")
self.zip_pattern = [Link](r'^\d{5}(-\d{4})?# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+

---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
```````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables
print(f"{name} is {age} years old") # Output: Alice is 30 years old
```````

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
```````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
```````

---
### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
```````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False

# Type conversion (casting)


str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
```````

---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
```````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
```````

---

### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
```````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
```````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.
**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
```````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")

# Enumerate - get both index and value


for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
```````

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
```````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
```````

---

### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
```````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
```````
---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
```````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


```````

---
## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
```````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"

message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


```````

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
```````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
```````

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
```````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100
# Practical example: Finding maximum
def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
```````

---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
```````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
```````

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
```````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
```````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec
**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
```````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
```````

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
```````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
```````

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
```````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"
def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)

print(book1) # Output: 'Python Basics' by John Doe


print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
```````
---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
```````python
# Parent class
class Animal:
"""Base class for all animals"""

def __init__(self, name, age):


[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
```````

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
```````python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance
@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
```````

---

### Properties

**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
```````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
```````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
```````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from
# Standard library examples
import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")

# Practical example: Using multiple imports


from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
```````

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


```````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""

@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
```````

Use it in another file:


```````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
```````

---
### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
```````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]

def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
```````

---

## 6. File Handling

### Reading Files


**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
```````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
```````

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
```````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
```````

---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
```````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Practical example: Safe file operations


def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
```````

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
```````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
```````

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
```````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
```````

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
```````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
```````

---

### Raising Exceptions

**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
```````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")

invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
```````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
```````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass
class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")

if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
```````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
```````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self
def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""

def __init__(self, filename, max_lines):


[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
```````

---

### Generators
**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
```````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5
# Practical example: Large file processing
def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
```````

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
```````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory
# Generator expression (creates values on demand)
squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
```````

---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
```````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
```````

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
```````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
```````

---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
```````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
```````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
```````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring

# Practical example: Complete decorator template


from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
""")

def validate_email(self, email):


return bool(self.email_pattern.match(email))

def validate_phone(self, phone):


return bool(self.phone_pattern.match(phone))

def validate_zip(self, zip_code):


return bool(self.zip_pattern.match(zip_code))
validator = Validator()
print(validator.validate_email("test@[Link]")) # True
print(validator.validate_phone("123-456-7890")) # True
print(validator.validate_zip("12345")) # True
print(validator.validate_zip("12345-6789")) # True
```````

---

### Advanced Regex Features

**Definition:** Advanced regex features include lookahead/lookbehind assertions, non-capturing groups, and flags for specia

**Why Use It:** Enables complex pattern matching, optimizes regex performance, and solves sophisticated text processing p

**Example:**
```````python
import re

# Flags
text = "Python is AWESOME"

# Case insensitive
result = [Link](r'python', text, [Link])
print(result) # ['Python']

# Multiline mode
multiline_text = """First line
Second line
Third line"""
matches = [Link](r'^.*line# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+

---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
``````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables
print(f"{name} is {age} years old") # Output: Alice is 30 years old
``````

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
``````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
``````

---
### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
``````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False

# Type conversion (casting)


str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
``````

---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
``````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
``````

---

### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
``````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
``````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.
**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
``````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")

# Enumerate - get both index and value


for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
``````

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
``````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
``````

---

### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
``````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
``````
---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
``````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


``````

---
## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
``````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"

message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


``````

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
``````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
``````

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
``````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100
# Practical example: Finding maximum
def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
``````

---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
``````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
``````

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
``````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
``````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec
**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
``````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
``````

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
``````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
``````

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
``````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"
def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)

print(book1) # Output: 'Python Basics' by John Doe


print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
``````
---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
``````python
# Parent class
class Animal:
"""Base class for all animals"""

def __init__(self, name, age):


[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
``````

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
``````python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance
@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
``````

---

### Properties

**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
``````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
``````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
``````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from
# Standard library examples
import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")

# Practical example: Using multiple imports


from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
``````

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


``````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""

@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
``````

Use it in another file:


``````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
``````

---
### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
``````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]

def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
``````

---

## 6. File Handling

### Reading Files


**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
``````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
``````

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
``````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
``````

---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
``````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Practical example: Safe file operations


def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
``````

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
``````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
``````

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
``````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
``````

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
``````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
``````

---

### Raising Exceptions

**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
``````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")

invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
``````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
``````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass
class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")

if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
``````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
``````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self
def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""

def __init__(self, filename, max_lines):


[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
``````

---

### Generators
**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
``````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5
# Practical example: Large file processing
def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
``````

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
``````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory
# Generator expression (creates values on demand)
squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
``````

---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
``````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
``````

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
``````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
``````

---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
``````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
``````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
``````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring

# Practical example: Complete decorator template


from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
""", multiline_text, [Link])
print(matches) # ['First line', 'Second line', 'Third line']

# Verbose mode (allows comments in regex)


complex_pattern = [Link](r'''
(\d{3}) # Area code
- # Separator
(\d{3}) # Prefix
- # Separator
(\d{4}) # Line number
''', [Link])
# Positive lookahead (?=...)
# Match password that contains at least one digit
text = "mypassword123"
if [Link](r'^(?=.*\d).+# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+

---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)
---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
`````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables

print(f"{name} is {age} years old") # Output: Alice is 30 years old


`````

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
`````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
`````

---

### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
`````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False

# Type conversion (casting)


str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
`````
---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
`````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
`````

---

### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
`````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
`````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.

**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
`````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")

# Enumerate - get both index and value


for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
`````

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
`````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
`````

---

### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
`````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
`````

---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
`````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


`````

---

## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
`````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"

message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15
# Function with no return (returns None)
def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


`````

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
`````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
`````
---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
`````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100

# Practical example: Finding maximum


def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
`````

---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
`````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
`````

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
`````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
`````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec

**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
`````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count
return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
`````

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
`````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
`````

---
### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
`````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"

def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)

print(book1) # Output: 'Python Basics' by John Doe


print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
`````

---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
`````python
# Parent class
class Animal:
"""Base class for all animals"""

def __init__(self, name, age):


[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"
# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
`````

---

### Class Methods and Static Methods


**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
`````python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance

@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
`````

---

### Properties

**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
`````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)
@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative
# Practical example: Temperature with validation
class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
`````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l
**Example:**
`````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from

# Standard library examples


import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")

# Practical example: Using multiple imports


from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))
save_log("Application started")
`````

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


`````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""

@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
`````

Use it in another file:


`````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
`````

---

### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
`````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]

def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
`````

---

## 6. File Handling

### Reading Files

**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
`````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)
# Practical example: Process CSV-like data
with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
`````

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
`````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
`````

---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
`````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Practical example: Safe file operations


def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
`````

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
`````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
`````

---

## 7. Exception Handling
### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
`````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
`````

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
`````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")
def execute(self, query):
if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
`````

---

### Raising Exceptions

**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
`````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")

invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
`````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me
**Example:**
`````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass

class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")

if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]
def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
`````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior
**Example:**
`````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""

def __init__(self, filename, max_lines):


[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
`````

---

### Generators

**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
`````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5

# Practical example: Large file processing


def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
`````

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
`````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory

# Generator expression (creates values on demand)


squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
`````

---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
`````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Tree traversal example
class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
`````

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
`````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b
result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
`````

---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
`````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
`````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
`````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring

# Practical example: Complete decorator template


from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
""", text):
print("Password contains a digit")

# Negative lookahead (?!...)


# Match words that don't start with 'test'
words = "hello test world testing done"
matches = [Link](r'\b(?!test)\w+', words)
print(matches) # ['hello', 'world', 'done']

# Positive lookbehind (?<=...)


# Extract price values after dollar sign
text = "Items cost $10, $25, and $100"
prices = [Link](r'(?<=\$)\d+', text)
print(prices) # ['10', '25', '100']

# Non-capturing group (?:...)


# Group without capturing
text = "[Link] and [Link]
urls = [Link](r'(?:http|https)://[\w.]+', text)
print(urls) # ['[Link] '[Link]

# Practical example: Password validation


def validate_strong_password(password):
"""
Password must:
- Be 8-20 characters
- Contain uppercase letter
- Contain lowercase letter
- Contain digit
- Contain special character
"""
pattern = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}# Complete Python Documentatio
## From Basics to Advanced - Python 3.13+

---

## Table of Contents
1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)
2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint
**Example:**
````python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables

print(f"{name} is {age} years old") # Output: Alice is 30 years old


````

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
````python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>
# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
````

---

### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
````python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False

# Type conversion (casting)


str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
````

---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro
**Example:**
````python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
````

---

### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
````python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
````

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.

**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
````python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")

# Enumerate - get both index and value


for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
````

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con
**Example:**
````python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
````

---

### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
````python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9
# Practical example: Finding first valid item
numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
````

---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
````python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


````

---

## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
````python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"

message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


````

---
### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
````python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
````

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
````python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100

# Practical example: Finding maximum


def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
````

---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
````python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC
# Practical example: Building database query
def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
````

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
````python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)
print(find_user(1)) # Output: Alice
print(find_user(99)) # Output: None
````

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec

**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
````python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)
# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
````

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
````python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"
# Creating objects (instances)
buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
````

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
````python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"

def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)

print(book1) # Output: 'Python Basics' by John Doe


print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
````

---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
````python
# Parent class
class Animal:
"""Base class for all animals"""

def __init__(self, name, age):


[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"
def fetch(self): # New method specific to Dog
return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
````

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
````python
class Date:
"""Represents a date"""
def __init__(self, year, month, day):
[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance

@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
````

---

### Properties

**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
````python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius
@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
````

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
````python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from

# Standard library examples


import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")

# Practical example: Using multiple imports


from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
````

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.
**Example:**

Create a file named `[Link]`:


````python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""

@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
````

Use it in another file:


````python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
````

---

### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
````python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]

def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!
# When you import it elsewhere:
# from utilities import process_data
# The test code does NOT run
````

---

## 6. File Handling

### Reading Files

**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
````python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
````

---
### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
````python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
````

---

### Context Managers (with statement)


**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
````python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())

# Practical example: Safe file operations


def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
````

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex
**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
````python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
````

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
````python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
````
---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
````python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
````

---

### Raising Exceptions

**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
````python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")

invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
````

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
````python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass

class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")

if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
````

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
````python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
# Custom iterator class
class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""

def __init__(self, filename, max_lines):


[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
````

---

### Generators

**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
````python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5

# Practical example: Large file processing


def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
````

---
### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
````python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory

# Generator expression (creates values on demand)


squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
````

---

### Yield From


**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
````python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
````

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
````python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
````

---

### Decorators with Parameters


**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
````python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError
# Practical example: Retry decorator
def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
````

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
````python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring

# Practical example: Complete decorator template


from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
"""
return bool([Link](pattern, password))

print(validate_strong_password("Weak")) # False
print(validate_strong_password("StrongPass1!")) # True
````

---

## 12. Collections & Data Structures

### Lists

**Definition:** Lists are ordered, mutable sequences that can contain elements of any type. Created using square brackets `[]

**Why Use It:** Most versatile Python data structure, supports dynamic sizing, allows duplicates, and provides extensive bui

**Example:**
````python
# Creating lists
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True]
nested = [[1, 2], [3, 4], [5, 6]]
empty = []

# Common list operations


fruits = ['apple', 'banana', 'cherry']

# Adding elements
[Link]('date') # Add to end
[Link](1, 'apricot') # Insert at index
[Link](['fig', 'grape']) # Add multiple

# Removing elements
[Link]('banana') # Remove by value
popped = [Link]() # Remove last (or by index)
del fruits[0] # Delete by index

# Accessing elements
first = fruits[0]
last = fruits[-1]
subset = fruits[1:3] # Slicing

# List methods
[Link]() # Sort in place
[Link]() # Reverse in place
count = [Link]('apple') # Count occurrences
index = [Link]('cherry') # Find index

# List comprehension (covered in detail later)


squares = [x**2 for x in range(10)]

# Practical example: Managing a task list


class TaskList:
def __init__(self):
[Link] = []

def add_task(self, task):


[Link](task)
print(f"Added: {task}")

def remove_task(self, task):


if task in [Link]:
[Link](task)
print(f"Removed: {task}")

def show_tasks(self):
if not [Link]:
print("No tasks")
for i, task in enumerate([Link], 1):
print(f"{i}. {task}")

todo = TaskList()
todo.add_task("Buy groceries")
todo.add_task("Write code")
todo.show_tasks()
````

---

### Tuples

**Definition:** Tuples are ordered, immutable sequences similar to lists but cannot be modified after creation. Created using

**Why Use It:** Immutability ensures data integrity, faster than lists, can be used as dictionary keys, and perfect for fixed col

**Example:**
````python
# Creating tuples
coordinates = (10, 20)
single = (1,) # Note the comma for single element
empty = ()
mixed = (1, "hello", 3.14)
# Unpacking tuples
x, y = coordinates
print(f"x={x}, y={y}") # Output: x=10, y=20

# Tuple methods (limited due to immutability)


numbers = (1, 2, 3, 2, 4, 2)
count = [Link](2) # Count occurrences
index = [Link](3) # Find index

# Tuples as dictionary keys


locations = {
(0, 0): "Origin",
(1, 0): "East",
(0, 1): "North"
}
print(locations[(0, 0)]) # Output: Origin

# Named tuples (more readable)


from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])


p = Point(10, 20)
print(p.x, p.y) # Output: 10 20
print(p[0], p[1]) # Also works

# Practical example: Function returning multiple values


def get_user_info():
"""Return multiple values as tuple"""
name = "Alice"
age = 30
email = "alice@[Link]"
return name, age, email # Returns tuple

name, age, email = get_user_info()


print(f"{name}: {email}")

# Database records as tuples


def fetch_users():
"""Simulate database query returning tuples"""
return [
(1, "Alice", "alice@[Link]"),
(2, "Bob", "bob@[Link]"),
(3, "Charlie", "charlie@[Link]")
]

for user_id, name, email in fetch_users():


print(f"User {user_id}: {name} ({email})")
````

---

### Dictionaries

**Definition:** Dictionaries are unordered collections of key-value pairs, providing fast lookups by key. Created using curly

**Why Use It:** Fast O(1) average-case lookup, models real-world relationships, perfect for mappings and configurations.

**Example:**
````python
# Creating dictionaries
person = {
'name': 'Alice',
'age': 30,
'city': 'NYC'
}

# Alternative creation methods


person2 = dict(name='Bob', age=25)
pairs = dict([('a', 1), ('b', 2)])

# Accessing values
name = person['name'] # KeyError if not exists
age = [Link]('age', 0) # Returns default if not exists

# Adding/modifying
person['email'] = 'alice@[Link]' # Add new key
person['age'] = 31 # Modify existing

# Removing
del person['city'] # Remove key
popped = [Link]('email', None) # Remove and return value

# Dictionary methods
keys = [Link]() # Get all keys
values = [Link]() # Get all values
items = [Link]() # Get key-value pairs

# Checking existence
if 'name' in person:
print("Name exists")

# Merging dictionaries (Python 3.9+)


dict1 = {'a': 1, 'b': 2}
dict2 = {'c': 3, 'd': 4}
merged = dict1 | dict2 # {'a': 1, 'b': 2, 'c': 3, 'd': 4}

# Practical example: Word counter


def count_words(text):
"""Count word frequencies"""
words = [Link]().split()
counts = {}
for word in words:
counts[word] = [Link](word, 0) + 1
return counts

text = "hello world hello python world"


frequencies = count_words(text)
print(frequencies) # {'hello': 2, 'world': 2, 'python': 1}

# User database example


users_db = {
'alice': {'password': 'secret123', 'role': 'admin'},
'bob': {'password': 'pass456', 'role': 'user'}
}

def authenticate(username, password):


user = users_db.get(username)
if user and user['password'] == password:
return f"Welcome {username}! Role: {user['role']}"
return "Authentication failed"

print(authenticate('alice', 'secret123'))
````

---

### Sets

**Definition:** Sets are unordered collections of unique elements, supporting mathematical set operations. Created using cur

**Why Use It:** Automatic duplicate removal, fast membership testing O(1), supports set mathematics (union, intersection, e

**Example:**
````python
# Creating sets
numbers = {1, 2, 3, 4, 5}
empty_set = set() # Note: {} creates empty dict, not set
from_list = set([1, 2, 2, 3, 3, 3]) # Duplicates removed

# Adding/removing elements
[Link](6) # Add single element
[Link]([7, 8, 9]) # Add multiple
[Link](1) # Remove (raises error if not exists)
[Link](10) # Remove (no error if not exists)
popped = [Link]() # Remove arbitrary element

# Set operations
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

union = a | b # or [Link](b)
# {1, 2, 3, 4, 5, 6, 7, 8}

intersection = a & b # or [Link](b)


# {4, 5}

difference = a - b # or [Link](b)
# {# Complete Python Documentation with Detailed Explanations
## From Basics to Advanced - Python 3.13+

---

## Table of Contents

1. [Basic Syntax & Data Types](#1-basic-syntax--data-types)


2. [Control Flow](#2-control-flow)
3. [Functions](#3-functions)
4. [Object-Oriented Programming](#4-object-oriented-programming)
5. [Modules & Packages](#5-modules--packages)
6. [File Handling](#6-file-handling)
7. [Exception Handling](#7-exception-handling)
8. [Iterators & Generators](#8-iterators--generators)
9. [Decorators](#9-decorators)
10. [Context Managers](#10-context-managers)
11. [Regular Expressions](#11-regular-expressions)
12. [Collections & Data Structures](#12-collections--data-structures)
13. [Comprehensions](#13-comprehensions)
14. [Lambda Functions](#14-lambda-functions)
15. [Built-in Functions](#15-built-in-functions)
16. [String Methods](#16-string-methods)
17. [List/Dict/Set Methods](#17-listdictset-methods)
18. [Type Hints & Annotations](#18-type-hints--annotations)
19. [Async/Await](#19-asyncawait-concurrency)
20. [Multithreading & Multiprocessing](#20-multithreading--multiprocessing)
21. [Memory Management](#21-memory-management)
22. [Metaclasses](#22-metaclasses)
23. [Descriptors](#23-descriptors)
24. [Property Decorators](#24-property-decorators)
25. [Abstract Base Classes](#25-abstract-base-classes)
26. [Protocol Classes](#26-protocol-classes)
27. [Dataclasses](#27-dataclasses)
28. [Enums](#28-enums)
29. [Path Operations](#29-path-operations)
30. [JSON & Serialization](#30-json--serialization)
31. [Database Operations](#31-database-operations)
32. [Testing](#32-testing-unittest-pytest)
33. [Performance Optimization](#33-performance-optimization)
34. [Design Patterns](#34-design-patterns)
35. [Advanced Topics](#35-advanced-topics)

---

## 1. Basic Syntax & Data Types

### Variables

**Definition:** Variables are named containers that store data values in memory. Python is dynamically typed, meaning you d

**Why Use It:** Variables allow you to store and manipulate data throughout your program, making code reusable and maint

**Example:**
```python
# Simple variable assignment
name = "Alice" # String variable
age = 30 # Integer variable
height = 5.7 # Float variable
is_student = False # Boolean variable

# Multiple assignment
x, y, z = 1, 2, 3 # Assign multiple values at once
a = b = c = 10 # Assign same value to multiple variables

print(f"{name} is {age} years old") # Output: Alice is 30 years old


```

---

### Data Types

**Definition:** Data types define the kind of value a variable can hold. Python has several built-in data types.

**Why Use It:** Different data types are optimized for different operations. Using the right type improves performance and p

**Common Data Types:**


- **int**: Whole numbers (e.g., 42, -10)
- **float**: Decimal numbers (e.g., 3.14, -0.5)
- **str**: Text strings (e.g., "Hello")
- **bool**: True/False values
- **None**: Represents absence of value

**Example:**
```python
# Integer
count = 100
print(type(count)) # <class 'int'>

# Float
price = 19.99
print(type(price)) # <class 'float'>

# String
message = "Hello, World!"
print(type(message)) # <class 'str'>

# Boolean
is_active = True
print(type(is_active)) # <class 'bool'>

# Complex numbers
complex_num = 3 + 4j
print(type(complex_num)) # <class 'complex'>

# None type
result = None
print(type(result)) # <class 'NoneType'>
```

---

### Type Checking and Conversion

**Definition:** Type checking verifies the data type of a variable. Type conversion transforms data from one type to another.

**Why Use It:** Ensures data integrity, prevents errors, and allows operations between different types.

**Example:**
```python
# Type checking
age = 25
print(isinstance(age, int)) # True - checks if age is an integer
print(isinstance(age, str)) # False
# Type conversion (casting)
str_number = "123"
number = int(str_number) # Convert string to integer
print(number + 10) # 133

float_number = float(number) # Convert integer to float


print(float_number) # 123.0

back_to_str = str(number) # Convert back to string


print(back_to_str + "456") # "123456" (string concatenation)
```

---

## 2. Control Flow

### If-Elif-Else Statements

**Definition:** Conditional statements that execute different code blocks based on whether conditions are true or false.

**Why Use It:** Allows your program to make decisions and execute different paths of code based on conditions, making pro

**Example:**
```python
# Grade calculator
score = 85

if score >= 90:


grade = 'A'
print("Excellent!")
elif score >= 80:
grade = 'B'
print("Good job!")
elif score >= 70:
grade = 'C'
print("Satisfactory")
elif score >= 60:
grade = 'D'
print("Needs improvement")
else:
grade = 'F'
print("Failed")

print(f"Your grade is: {grade}") # Output: Good job! Your grade is: B
```

---
### Ternary Operator

**Definition:** A concise way to write simple if-else statements in a single line.

**Why Use It:** Makes code more readable and compact for simple conditional assignments.

**Example:**
```python
# Traditional if-else
age = 20
if age >= 18:
status = "Adult"
else:
status = "Minor"

# Ternary operator (more concise)


status = "Adult" if age >= 18 else "Minor"
print(status) # Output: Adult

# Practical example: Setting discount


price = 100
discount = 20 if price > 50 else 10
final_price = price - discount
print(f"Final price: ${final_price}") # Output: Final price: $80
```

---

### For Loops

**Definition:** A loop that iterates over a sequence (list, tuple, string, range) and executes a block of code for each item.

**Why Use It:** Automates repetitive tasks, processes collections of data, and eliminates the need for manual repetition.

**Example:**
```python
# Basic for loop with range
for i in range(5):
print(f"Count: {i}")
# Output: Count: 0, Count: 1, Count: 2, Count: 3, Count: 4

# Iterate over a list


fruits = ['apple', 'banana', 'cherry', 'date']
for fruit in fruits:
print(f"I like {fruit}")
# Enumerate - get both index and value
for index, fruit in enumerate(fruits):
print(f"{index + 1}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherry
# 4. date

# Loop with step


for i in range(0, 10, 2): # Start at 0, stop before 10, step by 2
print(i) # Output: 0, 2, 4, 6, 8
```

---

### While Loops

**Definition:** A loop that continues executing as long as a condition remains true.

**Why Use It:** Useful when you don't know in advance how many iterations are needed, or when waiting for a specific con

**Example:**
```python
# Basic while loop
count = 0
while count < 5:
print(f"Count is: {count}")
count += 1

# Practical example: User input validation


password = ""
while len(password) < 8:
password = input("Enter a password (min 8 characters): ")
if len(password) < 8:
print("Password too short. Try again.")
print("Password accepted!")

# Infinite loop with break condition


while True:
user_input = input("Type 'quit' to exit: ")
if user_input == 'quit':
break
print(f"You entered: {user_input}")
```

---
### Break and Continue

**Definition:**
- **break**: Exits the loop entirely
- **continue**: Skips the current iteration and moves to the next one

**Why Use It:** Provides fine control over loop execution, allowing you to skip unwanted iterations or exit early when condi

**Example:**
```python
# Break - exit loop when condition met
for i in range(10):
if i == 5:
break # Stop loop when i equals 5
print(i) # Output: 0, 1, 2, 3, 4

# Continue - skip certain iterations


for i in range(10):
if i % 2 == 0: # Skip even numbers
continue
print(i) # Output: 1, 3, 5, 7, 9

# Practical example: Finding first valid item


numbers = [0, -5, 3, -2, 8, 15]
for num in numbers:
if num <= 0:
continue # Skip non-positive numbers
if num > 10:
break # Stop if number too large
print(f"Valid number: {num}")
# Output: Valid number: 3, Valid number: 8
```

---

### Match-Case (Python 3.10+)

**Definition:** A structural pattern matching statement that compares a value against multiple patterns, similar to switch-case

**Why Use It:** Provides cleaner, more readable code than multiple if-elif statements, especially for complex pattern matchin

**Example:**
```python
# HTTP status code handler
def handle_response(status_code):
match status_code:
case 200:
return "Success"
case 404:
return "Not Found"
case 500 | 502 | 503: # Multiple values
return "Server Error"
case code if 400 <= code < 500: # With condition
return "Client Error"
case _: # Default case
return "Unknown Status"

print(handle_response(200)) # Output: Success


print(handle_response(403)) # Output: Client Error

# Pattern matching with data structures


def process_command(command):
match [Link]():
case ["quit"]:
return "Exiting program"
case ["load", filename]:
return f"Loading {filename}"
case ["save", filename]:
return f"Saving to {filename}"
case ["move", direction] if direction in ["up", "down", "left", "right"]:
return f"Moving {direction}"
case _:
return "Unknown command"

print(process_command("load [Link]")) # Output: Loading [Link]


```

---

## 3. Functions

### Basic Functions

**Definition:** A reusable block of code that performs a specific task. Functions are defined using the `def` keyword.

**Why Use It:** Promotes code reusability, organization, and maintainability. Breaks complex problems into smaller, manage

**Example:**
```python
# Simple function
def greet(name):
"""Greets a person by name"""
return f"Hello, {name}!"
message = greet("Alice")
print(message) # Output: Hello, Alice!

# Function with multiple parameters


def calculate_area(length, width):
"""Calculates rectangle area"""
area = length * width
return area

result = calculate_area(5, 3)
print(f"Area: {result}") # Output: Area: 15

# Function with no return (returns None)


def print_welcome():
print("Welcome to Python!")
# No return statement

print_welcome() # Output: Welcome to Python!


```

---

### Default Arguments

**Definition:** Parameters that have default values assigned, making them optional when calling the function.

**Why Use It:** Makes functions more flexible and reduces the need for multiple function definitions for similar tasks.

**Example:**
```python
# Function with default parameter
def power(base, exponent=2):
"""Raises base to the power of exponent (default: 2)"""
return base ** exponent

print(power(5)) # Uses default exponent=2, Output: 25


print(power(5, 3)) # Custom exponent, Output: 125

# Practical example: Greeting with default


def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

print(greet("Alice")) # Output: Hello, Alice!


print(greet("Bob", "Good morning")) # Output: Good morning, Bob!

# Multiple defaults
def create_profile(name, age=18, country="USA"):
return {
'name': name,
'age': age,
'country': country
}

print(create_profile("Alice")) # Uses all defaults


print(create_profile("Bob", 25)) # Overrides age
print(create_profile("Charlie", country="UK")) # Skip age, set country
```

---

### Variable Arguments (*args)

**Definition:** Allows a function to accept any number of positional arguments, which are collected into a tuple.

**Why Use It:** Makes functions flexible when you don't know in advance how many arguments will be passed.

**Example:**
```python
# Function accepting any number of arguments
def sum_all(*args):
"""Sums all provided numbers"""
total = 0
for num in args:
total += num
return total

print(sum_all(1, 2, 3)) # Output: 6


print(sum_all(10, 20, 30, 40)) # Output: 100

# Practical example: Finding maximum


def find_max(*numbers):
"""Finds the maximum among any number of values"""
if not numbers:
return None
max_val = numbers[0]
for num in numbers:
if num > max_val:
max_val = num
return max_val

print(find_max(5, 12, 3, 9)) # Output: 12


print(find_max(100)) # Output: 100
```
---

### Keyword Arguments (**kwargs)

**Definition:** Allows a function to accept any number of keyword arguments, which are collected into a dictionary.

**Why Use It:** Provides flexibility for functions that need to handle varying named parameters, useful for configuration and

**Example:**
```python
# Function accepting keyword arguments
def print_info(**kwargs):
"""Prints all key-value pairs"""
for key, value in [Link]():
print(f"{key}: {value}")

print_info(name="Alice", age=30, city="NYC")


# Output:
# name: Alice
# age: 30
# city: NYC

# Practical example: Building database query


def build_query(table, **conditions):
"""Builds a SQL-like query string"""
query = f"SELECT * FROM {table}"
if conditions:
where_clause = " AND ".join([f"{k}='{v}'" for k, v in [Link]()])
query += f" WHERE {where_clause}"
return query

print(build_query("users", age=30, city="NYC"))


# Output: SELECT * FROM users WHERE age='30' AND city='NYC'
```

---

### Function Annotations (Type Hints)

**Definition:** Optional metadata that specifies the expected types of function parameters and return values.

**Why Use It:** Improves code documentation, enables static type checking with tools like mypy, and makes code more mai

**Example:**
```python
# Function with type hints
def add_numbers(x: int, y: int) -> int:
"""Adds two integers and returns an integer"""
return x + y

result = add_numbers(5, 3)
print(result) # Output: 8

# More complex type hints


from typing import List, Dict, Optional

def process_names(names: List[str]) -> Dict[str, int]:


"""Returns dictionary with name lengths"""
return {name: len(name) for name in names}

result = process_names(["Alice", "Bob", "Charlie"])


print(result) # Output: {'Alice': 5, 'Bob': 3, 'Charlie': 7}

# Optional return type


def find_user(user_id: int) -> Optional[str]:
"""Returns username if found, None otherwise"""
users = {1: "Alice", 2: "Bob"}
return [Link](user_id)

print(find_user(1)) # Output: Alice


print(find_user(99)) # Output: None
```

---

### Closures and Nested Functions

**Definition:** A closure is a function that remembers values from its enclosing scope even after that scope has finished exec

**Why Use It:** Enables data encapsulation, creates function factories, and allows for elegant callback patterns.

**Example:**
```python
# Basic closure
def outer_function(x):
"""Outer function that returns an inner function"""
def inner_function(y):
"""Inner function that remembers x"""
return x + y
return inner_function

# Create a closure
add_5 = outer_function(5)
print(add_5(10)) # Output: 15 (remembers x=5)
print(add_5(20)) # Output: 25

# Practical example: Counter factory


def make_counter():
"""Creates a counter function"""
count = 0

def increment():
nonlocal count # Modify outer scope variable
count += 1
return count

return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter2()) # Output: 1 (separate counter)

# Multiplier factory
def make_multiplier(n):
"""Creates a function that multiplies by n"""
def multiply(x):
return x * n
return multiply

times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_3(10)) # Output: 30
print(times_5(10)) # Output: 50
```

---

## 4. Object-Oriented Programming

### Classes and Objects

**Definition:** A class is a blueprint for creating objects. Objects are instances of classes that combine data (attributes) and b

**Why Use It:** Organizes code into reusable components, models real-world entities, and implements encapsulation, inherit

**Example:**
```python
# Basic class definition
class Dog:
"""Represents a dog"""

# Class attribute (shared by all instances)


species = "Canis familiaris"

# Constructor (initializer)
def __init__(self, name, age):
"""Initialize a new dog"""
[Link] = name # Instance attribute
[Link] = age

# Instance method
def bark(self):
"""Make the dog bark"""
return f"{[Link]} says Woof!"

def get_info(self):
"""Return dog information"""
return f"{[Link]} is {[Link]} years old"

# Creating objects (instances)


buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)

print([Link]()) # Output: Buddy says Woof!


print(max_dog.get_info()) # Output: Max is 5 years old
print([Link]) # Output: Canis familiaris

# Practical example: Bank Account


class BankAccount:
"""Represents a bank account"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance

def deposit(self, amount):


"""Add money to account"""
if amount > 0:
[Link] += amount
return f"Deposited ${amount}. New balance: ${[Link]}"
return "Invalid amount"

def withdraw(self, amount):


"""Remove money from account"""
if amount > [Link]:
return "Insufficient funds"
[Link] -= amount
return f"Withdrew ${amount}. New balance: ${[Link]}"

account = BankAccount("Alice", 1000)


print([Link](500)) # Output: Deposited $500. New balance: $1500
print([Link](200)) # Output: Withdrew $200. New balance: $1300
```

---

### Magic Methods (Dunder Methods)

**Definition:** Special methods with double underscores (e.g., `__init__`, `__str__`) that define how objects behave with bui

**Why Use It:** Allows custom classes to work seamlessly with Python's built-in functions and operators, making objects be

**Example:**
```python
class Book:
"""Represents a book"""

def __init__(self, title, author, pages):


[Link] = title
[Link] = author
[Link] = pages

def __str__(self):
"""String representation for users"""
return f"'{[Link]}' by {[Link]}"

def __repr__(self):
"""String representation for developers"""
return f"Book(title='{[Link]}', author='{[Link]}', pages={[Link]})"

def __len__(self):
"""Return number of pages"""
return [Link]

def __eq__(self, other):


"""Check if two books are equal"""
return [Link] == [Link] and [Link] == [Link]

book1 = Book("Python Basics", "John Doe", 300)


book2 = Book("Python Basics", "John Doe", 300)
print(book1) # Output: 'Python Basics' by John Doe
print(repr(book1)) # Output: Book(title='Python Basics'...)
print(len(book1)) # Output: 300
print(book1 == book2) # Output: True

# Arithmetic magic methods


class Vector:
"""Represents a 2D vector"""

def __init__(self, x, y):


self.x = x
self.y = y

def __add__(self, other):


"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)

def __mul__(self, scalar):


"""Multiply vector by scalar"""
return Vector(self.x * scalar, self.y * scalar)

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Uses __add__
v4 = v1 * 3 # Uses __mul__

print(v3) # Output: Vector(6, 8)


print(v4) # Output: Vector(6, 9)
```

---

### Inheritance

**Definition:** A mechanism where a new class (child/subclass) derives properties and methods from an existing class (paren

**Why Use It:** Promotes code reuse, creates hierarchical relationships, and allows for polymorphism (same interface, differ

**Example:**
```python
# Parent class
class Animal:
"""Base class for all animals"""
def __init__(self, name, age):
[Link] = name
[Link] = age

def speak(self):
"""Generic speak method"""
return "Some sound"

def info(self):
return f"{[Link]} is {[Link]} years old"

# Child class
class Dog(Animal):
"""Dog class inherits from Animal"""

def __init__(self, name, age, breed):


super().__init__(name, age) # Call parent constructor
[Link] = breed

def speak(self): # Override parent method


return "Woof!"

def fetch(self): # New method specific to Dog


return f"{[Link]} is fetching the ball"

class Cat(Animal):
"""Cat class inherits from Animal"""

def speak(self):
return "Meow!"

def scratch(self):
return f"{[Link]} is scratching"

# Using inherited classes


dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)

print([Link]()) # Inherited method: Buddy is 3 years old


print([Link]()) # Overridden method: Woof!
print([Link]()) # New method: Buddy is fetching the ball

print([Link]()) # Overridden method: Meow!


print([Link]()) # New method: Whiskers is scratching

# Polymorphism - same interface, different behavior


animals = [dog, cat]
for animal in animals:
print(f"{[Link]} says: {[Link]()}")
# Output:
# Buddy says: Woof!
# Whiskers says: Meow!
```

---

### Class Methods and Static Methods

**Definition:**
- **Class methods**: Methods that receive the class as the first parameter (cls), not an instance
- **Static methods**: Methods that don't receive class or instance, just regular functions within class namespace

**Why Use It:** Class methods are useful for factory methods and alternative constructors. Static methods are utility function

**Example:**
```python
class Date:
"""Represents a date"""

def __init__(self, year, month, day):


[Link] = year
[Link] = month
[Link] = day

@classmethod
def from_string(cls, date_string):
"""Factory method: Create Date from string"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day) # Returns new instance

@classmethod
def today(cls):
"""Factory method: Create Date for today"""
import datetime
today = [Link]()
return cls([Link], [Link], [Link])

@staticmethod
def is_leap_year(year):
"""Utility function: Check if year is leap year"""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def __str__(self):
return f"{[Link]}-{[Link]:02d}-{[Link]:02d}"

# Using regular constructor


date1 = Date(2024, 3, 15)
print(date1) # Output: 2024-03-15

# Using class method factory


date2 = Date.from_string("2024-12-25")
print(date2) # Output: 2024-12-25

# Using static method (no instance needed)


print(Date.is_leap_year(2024)) # Output: True
print(Date.is_leap_year(2023)) # Output: False

# Practical example: Temperature converter


class Temperature:
"""Temperature converter"""

def __init__(self, celsius):


[Link] = celsius

@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Create Temperature from Fahrenheit"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)

@staticmethod
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit"""
return (celsius * 9/5) + 32

def __str__(self):
return f"{[Link]}°C"

temp1 = Temperature(25)
temp2 = Temperature.from_fahrenheit(77)

print(temp1) # Output: 25°C


print(temp2) # Output: 25.0°C
print(Temperature.celsius_to_fahrenheit(25)) # Output: 77.0
```

---

### Properties
**Definition:** Properties allow you to define methods that can be accessed like attributes, providing controlled access to cla

**Why Use It:** Enables encapsulation, data validation, computed attributes, and maintains a clean interface while adding log

**Example:**
```python
class Circle:
"""Represents a circle"""

def __init__(self, radius):


self._radius = radius # Private attribute (by convention)

@property
def radius(self):
"""Getter for radius"""
return self._radius

@[Link]
def radius(self, value):
"""Setter with validation"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value

@property
def diameter(self):
"""Computed property"""
return self._radius * 2

@property
def area(self):
"""Computed property"""
import math
return [Link] * (self._radius ** 2)

@property
def circumference(self):
"""Computed property"""
import math
return 2 * [Link] * self._radius

# Using properties
circle = Circle(5)

# Access like attributes (calls getter)


print(f"Radius: {[Link]}") # Output: Radius: 5
print(f"Diameter: {[Link]}") # Output: Diameter: 10
print(f"Area: {[Link]:.2f}") # Output: Area: 78.54

# Set like attribute (calls setter with validation)


[Link] = 10
print(f"New radius: {[Link]}") # Output: New radius: 10

# Validation works
try:
[Link] = -5
except ValueError as e:
print(f"Error: {e}") # Output: Error: Radius cannot be negative

# Practical example: Temperature with validation


class Thermostat:
"""Temperature controller"""

def __init__(self, celsius=20):


self._celsius = celsius

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
if value > 100:
print("Warning: Very high temperature!")
self._celsius = value

@property
def fahrenheit(self):
"""Convert to Fahrenheit on the fly"""
return (self._celsius * 9/5) + 32

@[Link]
def fahrenheit(self, value):
"""Set temperature in Fahrenheit"""
[Link] = (value - 32) * 5/9

thermostat = Thermostat()
print(f"Current: {[Link]}°C") # Output: Current: 20°C
print(f"In Fahrenheit: {[Link]}°F") # Output: In Fahrenheit: 68.0°F

[Link] = 86 # Set using Fahrenheit


print(f"Now: {[Link]}°C") # Output: Now: 30.0°C
```

---

## 5. Modules & Packages

### Importing Modules

**Definition:** Modules are Python files containing functions, classes, and variables. Importing allows you to use code from

**Why Use It:** Organizes code into logical units, promotes code reuse, and provides access to Python's extensive standard l

**Example:**
```python
# Different ways to import

# 1. Import entire module


import math
result = [Link](16)
print(result) # Output: 4.0

# 2. Import specific items


from datetime import datetime, timedelta
now = [Link]()
print(now)

# 3. Import with alias


import numpy as np # Common convention for numpy
import pandas as pd # Common convention for pandas

# 4. Import all (not recommended - pollutes namespace)


from math import *
print(pi) # Works but unclear where pi comes from

# Standard library examples


import random
import os
from pathlib import Path
from collections import Counter, defaultdict

# Using imported modules


random_num = [Link](1, 100)
print(f"Random number: {random_num}")

current_dir = [Link]()
print(f"Current directory: {current_dir}")
# Practical example: Using multiple imports
from datetime import datetime
import json

def save_log(message):
"""Save timestamped log message"""
log_entry = {
'timestamp': [Link]().isoformat(),
'message': message
}
print([Link](log_entry, indent=2))

save_log("Application started")
```

---

### Creating Your Own Modules

**Definition:** Any Python file can be a module. You create one by saving Python code in a `.py` file and importing it in othe

**Why Use It:** Organizes your code into reusable components, separates concerns, and makes large projects manageable.

**Example:**

Create a file named `[Link]`:


```python
# [Link]
"""Custom math utilities"""

PI = 3.14159

def circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2

def circle_circumference(radius):
"""Calculate circle circumference"""
return 2 * PI * radius

def square_area(side):
"""Calculate square area"""
return side ** 2

class Calculator:
"""Simple calculator class"""
@staticmethod
def add(a, b):
return a + b

@staticmethod
def multiply(a, b):
return a * b
```

Use it in another file:


```python
# [Link]
import mymath

# Use module's constant


print(f"PI value: {[Link]}")

# Use module's functions


area = mymath.circle_area(5)
print(f"Circle area: {area}")

# Use module's class


calc = [Link]()
result = [Link](10, 20)
print(f"10 + 20 = {result}")

# Alternative import style


from mymath import circle_area, PI
print(circle_area(3))
```

---

### The __name__ Variable

**Definition:** `__name__` is a special variable that equals `"__main__"` when the file is run directly, or the module name w

**Why Use It:** Allows you to write code that runs only when the file is executed directly, not when imported. Essential for c

**Example:**
```python
# [Link]
"""Utility functions"""

def process_data(data):
"""Process data"""
return [x * 2 for x in data]
def validate_input(value):
"""Validate input"""
return value > 0

# This code only runs when file is executed directly


if __name__ == "__main__":
# Test code
print("Testing utilities module...")

test_data = [1, 2, 3, 4, 5]
result = process_data(test_data)
print(f"Test result: {result}")

print(f"Validation test: {validate_input(10)}")


print("All tests passed!")

# When you run: python [Link]


# Output: Testing utilities module...
# Test result: [2, 4, 6, 8, 10]
# Validation test: True
# All tests passed!

# When you import it elsewhere:


# from utilities import process_data
# The test code does NOT run
```

---

## 6. File Handling

### Reading Files

**Definition:** File reading operations allow you to access and read content from files stored on disk.

**Why Use It:** Essential for data processing, configuration loading, log analysis, and working with persistent data.

**Example:**
```python
# Method 1: Read entire file
with open('[Link]', 'r') as file:
content = [Link]()
print(content)

# Method 2: Read line by line (memory efficient)


with open('[Link]', 'r') as file:
for line in file:
print([Link]()) # strip() removes newline characters

# Method 3: Read all lines into a list


with open('[Link]', 'r') as file:
lines = [Link]()
print(f"Total lines: {len(lines)}")

# Method 4: Read specific number of characters


with open('[Link]', 'r') as file:
first_100_chars = [Link](100)
print(first_100_chars)

# Practical example: Process CSV-like data


with open('[Link]', 'r') as file:
for line in file:
if [Link](): # Skip empty lines
name, age = [Link]().split(',')
print(f"{name} is {age} years old")
```

---

### Writing Files

**Definition:** File writing operations allow you to create new files or modify existing ones by writing data to disk.

**Why Use It:** Saves program output, creates logs, generates reports, and persists data between program runs.

**Example:**
```python
# Write mode ('w') - overwrites existing file
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n")
[Link]("This is line 2\n")

# Append mode ('a') - adds to end of file


with open('[Link]', 'a') as file:
[Link]("This line is appended\n")

# Write multiple lines at once


lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
with open('[Link]', 'w') as file:
[Link](lines)

# Practical example: Save user data


users = [
{'name': 'Alice', 'score': 95},
{'name': 'Bob', 'score': 87},
{'name': 'Charlie', 'score': 92}
]

with open('[Link]', 'w') as file:


for user in users:
[Link](f"{user['name']}: {user['score']}\n")

# Write formatted report


with open('[Link]', 'w') as file:
[Link]("=" * 40 + "\n")
[Link]("SALES REPORT\n")
[Link]("=" * 40 + "\n")
[Link](f"Total Sales: $1,234,567\n")
[Link](f"Items Sold: 5,432\n")
```

---

### Context Managers (with statement)

**Definition:** The `with` statement automatically handles resource setup and cleanup, ensuring files are properly closed eve

**Why Use It:** Prevents resource leaks, ensures proper cleanup, and makes code more readable and reliable.

**Example:**
```python
# Without context manager (not recommended)
file = open('[Link]', 'r')
try:
content = [Link]()
print(content)
finally:
[Link]() # Must remember to close

# With context manager (recommended)


with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File automatically closed, even if exception occurs

# Multiple files at once


with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())
# Practical example: Safe file operations
def process_file(filename):
"""Safely process file with error handling"""
try:
with open(filename, 'r') as file:
data = [Link]()
# Process data
result = [Link]()
return result
except FileNotFoundError:
return f"Error: {filename} not found"
except PermissionError:
return f"Error: No permission to read {filename}"

print(process_file('[Link]'))
```

---

### Binary Files

**Definition:** Binary mode reads/writes files as raw bytes rather than text, used for non-text files like images, videos, and ex

**Why Use It:** Required for working with binary file formats, preserves exact byte content, and prevents text encoding issu

**Example:**
```python
# Reading binary file
with open('[Link]', 'rb') as file:
image_data = [Link]()
print(f"Image size: {len(image_data)} bytes")

# Writing binary file


with open('[Link]', 'wb') as file:
[Link](b'\x00\x01\x02\x03')

# Copying a binary file


def copy_binary_file(source, destination):
"""Copy file in binary mode"""
with open(source, 'rb') as src, open(destination, 'wb') as dst:
[Link]([Link]())

# Practical example: Read image metadata


def get_file_signature(filename):
"""Read first few bytes (file signature)"""
with open(filename, 'rb') as file:
signature = [Link](8)
return [Link]()

# JPEG files start with FFD8


# PNG files start with 89504E47
signature = get_file_signature('[Link]')
print(f"File signature: {signature}")
```

---

## 7. Exception Handling

### Try-Except Blocks

**Definition:** Exception handling allows you to gracefully handle errors that occur during program execution, preventing cr

**Why Use It:** Makes programs robust, provides user-friendly error messages, and allows recovery from errors.

**Example:**
```python
# Basic exception handling
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = None

# Multiple exception types


try:
number = int("abc")
except ValueError:
print("Invalid number format")
except TypeError:
print("Type error occurred")

# Catch multiple exceptions together


try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")

# Practical example: Safe user input


def get_positive_number():
"""Get positive number with validation"""
while True:
try:
value = int(input("Enter a positive number: "))
if value <= 0:
print("Number must be positive!")
continue
return value
except ValueError:
print("Invalid input! Please enter a number.")

# File handling with exceptions


def read_config(filename):
"""Read configuration file safely"""
try:
with open(filename, 'r') as file:
return [Link]()
except FileNotFoundError:
print(f"Config file {filename} not found. Using defaults.")
return "{}"
except PermissionError:
print(f"No permission to read {filename}")
return None
```

---

### Try-Except-Else-Finally

**Definition:**
- **else**: Runs if no exception occurred
- **finally**: Always runs, regardless of exceptions (cleanup code)

**Why Use It:** Provides precise control over exception handling flow, ensures cleanup code runs, and separates success log

**Example:**
```python
# Complete exception handling structure
try:
file = open('[Link]', 'r')
data = [Link]()
number = int(data)
except FileNotFoundError:
print("File not found")
except ValueError:
print("File contains invalid data")
else:
# Runs only if no exception occurred
print(f"Successfully read number: {number}")
finally:
# Always runs (cleanup)
if 'file' in locals():
[Link]()
print("File closed")

# Practical example: Database connection


class DatabaseConnection:
"""Simulated database connection"""

def connect(self):
print("Connecting to database...")

def execute(self, query):


if "DROP" in query:
raise ValueError("DROP commands not allowed")
print(f"Executing: {query}")

def close(self):
print("Closing database connection")

def run_query(query):
"""Execute query with proper cleanup"""
db = DatabaseConnection()
try:
[Link]()
[Link](query)
except ValueError as e:
print(f"Query error: {e}")
return False
else:
print("Query executed successfully")
return True
finally:
[Link]()

run_query("SELECT * FROM users")


# Output:
# Connecting to database...
# Executing: SELECT * FROM users
# Query executed successfully
# Closing database connection
```

---

### Raising Exceptions


**Definition:** You can manually trigger exceptions using the `raise` keyword to signal error conditions.

**Why Use It:** Enforces business logic, validates inputs, and creates clear error boundaries in your code.

**Example:**
```python
# Raise built-in exception
def calculate_percentage(value, total):
"""Calculate percentage"""
if total == 0:
raise ZeroDivisionError("Total cannot be zero")
if value < 0 or total < 0:
raise ValueError("Values must be non-negative")
return (value / total) * 100

# Using the function


try:
result = calculate_percentage(50, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")

# Re-raising exceptions
def process_data(data):
"""Process data with logging"""
try:
result = int(data)
return result * 2
except ValueError:
print("Logging error...")
raise # Re-raise the same exception

# Practical example: Age validation


def set_age(age):
"""Set age with validation"""
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age is unrealistic")
return age

# Using validation
try:
valid_age = set_age(25)
print(f"Age set to: {valid_age}")
invalid_age = set_age(-5)
except ValueError as e:
print(f"Validation error: {e}")
```

---

### Custom Exceptions

**Definition:** You can create your own exception classes by inheriting from the `Exception` class or its subclasses.

**Why Use It:** Creates domain-specific errors, provides better error context, and makes error handling more precise and me

**Example:**
```python
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account has insufficient funds"""
pass

# Custom exception with data


class ValidationError(Exception):
"""Raised when validation fails"""

def __init__(self, field, message):


[Link] = field
[Link] = message
super().__init__(f"{field}: {message}")

# Practical example: Bank account with custom exceptions


class AccountLockedError(Exception):
"""Raised when account is locked"""
pass

class BankAccount:
"""Bank account with custom exception handling"""

def __init__(self, owner, balance=0):


[Link] = owner
[Link] = balance
[Link] = False

def withdraw(self, amount):


"""Withdraw money with validations"""
if [Link]:
raise AccountLockedError("Account is locked")
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")

if amount > [Link]:


raise InsufficientFundsError(
f"Insufficient funds. Balance: ${[Link]}, "
f"Requested: ${amount}"
)

[Link] -= amount
return [Link]

def lock(self):
"""Lock the account"""
[Link] = True

# Using custom exceptions


account = BankAccount("Alice", 1000)

try:
[Link](1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

try:
[Link]()
[Link](100)
except AccountLockedError as e:
print(f"Cannot process: {e}")

# Validation with custom exceptions


def validate_user_registration(username, email, age):
"""Validate user registration data"""
if len(username) < 3:
raise ValidationError("username", "Must be at least 3 characters")

if "@" not in email:


raise ValidationError("email", "Invalid email format")

if age < 18:


raise ValidationError("age", "Must be 18 or older")

return True

try:
validate_user_registration("AB", "invalidemail", 16)
except ValidationError as e:
print(f"Registration failed - {e}")
```

---

## 8. Iterators & Generators

### Iterators

**Definition:** An iterator is an object that implements the iterator protocol (`__iter__()` and `__next__()` methods), allowin

**Why Use It:** Provides a standard way to loop through data, enables lazy evaluation, and allows custom iteration behavior

**Example:**
```python
# Basic iterator usage
my_list = [1, 2, 3, 4, 5]
iterator = iter(my_list)

print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3

# Custom iterator class


class Countdown:
"""Iterator that counts down from a number"""

def __init__(self, start):


[Link] = start

def __iter__(self):
return self

def __next__(self):
if [Link] <= 0:
raise StopIteration
[Link] -= 1
return [Link] + 1

# Using custom iterator


for num in Countdown(5):
print(num) # Output: 5, 4, 3, 2, 1

# Practical example: File line iterator with limit


class LimitedFileReader:
"""Read only N lines from a file"""
def __init__(self, filename, max_lines):
[Link] = filename
self.max_lines = max_lines
self.line_count = 0
[Link] = None

def __iter__(self):
[Link] = open([Link], 'r')
self.line_count = 0
return self

def __next__(self):
if self.line_count >= self.max_lines:
[Link]()
raise StopIteration

line = [Link]()
if not line:
[Link]()
raise StopIteration

self.line_count += 1
return [Link]()

# Read first 10 lines


for line in LimitedFileReader('large_file.txt', 10):
print(line)
```

---

### Generators

**Definition:** Generators are functions that use `yield` to produce a sequence of values lazily, one at a time, instead of retur

**Why Use It:** Memory efficient for large datasets, creates infinite sequences, simplifies iterator creation, and enables pipel

**Example:**
```python
# Basic generator function
def simple_generator():
"""Yields three values"""
print("First yield")
yield 1
print("Second yield")
yield 2
print("Third yield")
yield 3

# Using generator
gen = simple_generator()
print(next(gen)) # Output: First yield, then 1
print(next(gen)) # Output: Second yield, then 2

# Generator with parameters


def fibonacci(n):
"""Generate first n Fibonacci numbers"""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

# Using Fibonacci generator


for num in fibonacci(10):
print(num, end=' ') # Output: 0 1 1 2 3 5 8 13 21 34

# Infinite generator
def infinite_counter(start=0):
"""Count infinitely from start"""
count = start
while True:
yield count
count += 1

# Using infinite generator with break


counter = infinite_counter(1)
for i in counter:
if i > 5:
break
print(i) # Output: 1, 2, 3, 4, 5

# Practical example: Large file processing


def read_large_file(filename):
"""Memory-efficient file reader"""
with open(filename, 'r') as file:
for line in file:
yield [Link]()

# Process file without loading into memory


def count_words_in_file(filename):
"""Count words using generator"""
total = 0
for line in read_large_file(filename):
total += len([Link]())
return total

# Generator pipeline example


def filter_even(numbers):
"""Filter even numbers"""
for num in numbers:
if num % 2 == 0:
yield num

def square_numbers(numbers):
"""Square each number"""
for num in numbers:
yield num ** 2

# Chain generators
numbers = range(10)
evens = filter_even(numbers)
squared = square_numbers(evens)
print(list(squared)) # Output: [0, 4, 16, 36, 64]
```

---

### Generator Expressions

**Definition:** A concise way to create generators using syntax similar to list comprehensions, but with parentheses instead o

**Why Use It:** More memory efficient than list comprehensions, perfect for one-time iterations, and cleaner syntax for simp

**Example:**
```python
# List comprehension (creates entire list in memory)
squares_list = [x**2 for x in range(1000000)] # Uses lots of memory

# Generator expression (creates values on demand)


squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory

# Using generator expression


for square in (x**2 for x in range(10)):
print(square, end=' ') # Output: 0 1 4 9 16 25 36 49 64 81

# Generator expression with condition


evens = (x for x in range(20) if x % 2 == 0)
print(list(evens)) # Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Practical example: Sum of squares


total = sum(x**2 for x in range(100))
print(f"Sum of squares: {total}")

# Memory comparison
import sys

list_comp = [x for x in range(10000)]


gen_exp = (x for x in range(10000))

print(f"List size: {[Link](list_comp)} bytes") # Large


print(f"Generator size: {[Link](gen_exp)} bytes") # Small

# Chaining generator expressions


numbers = range(100)
evens = (x for x in numbers if x % 2 == 0)
doubled = (x * 2 for x in evens)
result = sum(doubled)
print(f"Result: {result}")
```

---

### Yield From

**Definition:** `yield from` delegates part of generator operations to another generator, simplifying code that chains generato

**Why Use It:** Makes generator delegation cleaner, flattens nested iterations, and improves code readability.

**Example:**
```python
# Without yield from (verbose)
def chain_generators_old(*iterables):
"""Chain iterables the old way"""
for iterable in iterables:
for item in iterable:
yield item

# With yield from (concise)


def chain_generators(*iterables):
"""Chain iterables using yield from"""
for iterable in iterables:
yield from iterable

# Using yield from


result = chain_generators([1, 2], [3, 4], [5, 6])
print(list(result)) # Output: [1, 2, 3, 4, 5, 6]

# Practical example: Flatten nested structure


def flatten(nested_list):
"""Recursively flatten nested lists"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item

nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]


flat = list(flatten(nested))
print(flat) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Tree traversal example


class TreeNode:
"""Simple tree node"""
def __init__(self, value, children=None):
[Link] = value
[Link] = children or []

def traverse(self):
"""Traverse tree using yield from"""
yield [Link]
for child in [Link]:
yield from [Link]()

# Create tree
root = TreeNode(1, [
TreeNode(2, [TreeNode(4), TreeNode(5)]),
TreeNode(3, [TreeNode(6)])
])

# Traverse
for value in [Link]():
print(value, end=' ') # Output: 1 2 4 5 3 6
```

---

## 9. Decorators

### Function Decorators

**Definition:** Decorators are functions that modify or enhance other functions without changing their source code. They "w

**Why Use It:** Adds reusable functionality (logging, timing, authentication), separates concerns, and keeps code DRY (Don

**Example:**
```python
# Basic decorator
def my_decorator(func):
"""Simple decorator that wraps a function"""
def wrapper():
print("Something before the function")
func()
print("Something after the function")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()
# Output:
# Something before the function
# Hello!
# Something after the function

# Decorator with arguments


def timing_decorator(func):
"""Measure function execution time"""
import time
def wrapper(*args, **kwargs):
start = [Link]()
result = func(*args, **kwargs)
end = [Link]()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
import time
[Link](1)
return "Done"

result = slow_function()
# Output: slow_function took 1.0001 seconds

# Practical example: Logging decorator


def log_function_call(func):
"""Log function calls with arguments"""
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(a) for a in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in [Link]())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))

print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper

@log_function_call
def add(a, b):
return a + b

result = add(5, 3)
# Output:
# Calling add(5, 3)
# add returned 8
```

---

### Decorators with Parameters

**Definition:** Decorators that accept arguments, requiring an extra layer of function nesting to configure behavior.

**Why Use It:** Allows customization of decorator behavior, makes decorators more flexible and reusable.

**Example:**
```python
# Decorator factory (decorator with parameters)
def repeat(times):
"""Repeat function execution N times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Validation decorator with parameters


def validate_range(min_val, max_val):
"""Validate function argument is in range"""
def decorator(func):
def wrapper(value):
if not (min_val <= value <= max_val):
raise ValueError(
f"Value {value} not in range [{min_val}, {max_val}]"
)
return func(value)
return wrapper
return decorator

@validate_range(0, 100)
def set_percentage(value):
return f"Percentage set to {value}%"

print(set_percentage(50)) # Works
# print(set_percentage(150)) # Raises ValueError

# Practical example: Retry decorator


def retry(max_attempts=3, delay=1):
"""Retry function on failure"""
import time

def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
print(f"Failed after {max_attempts} attempts")
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
[Link](delay)
return wrapper
return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if [Link]() < 0.7:
raise ConnectionError("Network error")
return "Success!"
```

---

### Preserving Function Metadata

**Definition:** Using `[Link]` preserves the original function's metadata (name, docstring) when creating decorator

**Why Use It:** Maintains proper function introspection, documentation, and debugging information.

**Example:**
```python
from functools import wraps

# Without @wraps (loses metadata)


def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

# With @wraps (preserves metadata)


def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def original_function():
"""This is the original function"""
pass

@bad_decorator
def bad_wrapped():
"""Original docstring"""
pass

@good_decorator
def good_wrapped():
"""Original docstring"""
pass

print(bad_wrapped.__name__) # Output: wrapper


print(good_wrapped.__name__) # Output: good_wrapped

print(bad_wrapped.__doc__) # Output: None


print(good_wrapped.__doc__) # Output: Original docstring
# Practical example: Complete decorator template
from functools import wraps

def my_decorator(func):
"""Decorator template with proper metadata preservation"""
@wraps(func)
def wrapper(*args, **kwargs):
# Before function
print(f"Calling {func.__name__}")

# Call function
result = func(*args, **kwargs)

# After function
print(f"Finished {func.__name__}")

return result
return wrapper

@my_decorator
def calculate(x, y):
"""

You might also like