Initial commit: Python by Example structure

This commit is contained in:
balshaer1 2025-12-30 08:50:00 +02:00
commit 10fc3566bb
35 changed files with 11535 additions and 0 deletions

223
01_basics/01_print.py Normal file
View File

@ -0,0 +1,223 @@
"""
================================================================================
File: 01_print.py
Topic: The print() Function in Python
================================================================================
This file demonstrates the print() function, which is used to display output
to the console. It's one of the most fundamental functions in Python and
essential for debugging and displaying information.
Key Concepts:
- Basic printing
- Multiple arguments
- Separators and end characters
- Formatted strings (f-strings)
- Escape characters
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Simple Output
# -----------------------------------------------------------------------------
# The most basic use of print()
print("--- Simple Output ---")
print("Hello, World!")
print("Welcome to Python!")
print("Learning is fun!")
# -----------------------------------------------------------------------------
# 2. Printing Different Data Types
# -----------------------------------------------------------------------------
# print() can output any data type
print("\n--- Different Data Types ---")
print(42) # Integer
print(3.14159) # Float
print(True) # Boolean
print(None) # NoneType
print([1, 2, 3]) # List
print({"a": 1}) # Dictionary
# -----------------------------------------------------------------------------
# 3. Printing Multiple Items
# -----------------------------------------------------------------------------
# Pass multiple arguments separated by commas
print("\n--- Multiple Items ---")
print("Hello", "World")
print("Python", "is", "awesome")
print("Name:", "Baraa", "| Age:", 25)
print(1, 2, 3, 4, 5)
# -----------------------------------------------------------------------------
# 4. The sep Parameter
# -----------------------------------------------------------------------------
# sep defines what goes between multiple items (default is space)
print("\n--- Separator Parameter ---")
print("Python", "Java", "C++", sep=", ")
print("2025", "01", "15", sep="-") # Date format
print("192", "168", "1", "1", sep=".") # IP address
print("apple", "banana", "cherry", sep=" | ")
print("a", "b", "c", sep="") # No separator
# -----------------------------------------------------------------------------
# 5. The end Parameter
# -----------------------------------------------------------------------------
# end defines what goes at the end (default is newline \n)
print("\n--- End Parameter ---")
print("Loading", end="")
print("...", end="")
print(" Done!")
print("First line", end=" --> ")
print("Second line")
# Creating a progress bar effect
print("\nProgress: ", end="")
for i in range(5):
print("", end="")
print(" Complete!")
# -----------------------------------------------------------------------------
# 6. Variables in Print
# -----------------------------------------------------------------------------
# Print variable values
print("\n--- Variables ---")
name = "Baraa"
age = 25
city = "Gaza"
is_student = True
print("Name:", name)
print("Age:", age)
print("City:", city)
print("Student:", is_student)
# Print with variable calculations
x = 10
y = 5
print("Sum:", x + y)
print("Product:", x * y)
# -----------------------------------------------------------------------------
# 7. String Formatting - f-strings (Recommended)
# -----------------------------------------------------------------------------
# Modern way to embed variables in strings (Python 3.6+)
print("\n--- f-strings (Formatted String Literals) ---")
name = "Baraa"
age = 25
height = 1.75
print(f"My name is {name}")
print(f"I am {age} years old")
print(f"Next year I'll be {age + 1}")
# Formatting numbers
pi = 3.14159265359
print(f"Pi to 2 decimals: {pi:.2f}")
print(f"Pi to 4 decimals: {pi:.4f}")
# Padding and alignment
print(f"{'Left':<10}|{'Center':^10}|{'Right':>10}")
print(f"{1:<10}|{2:^10}|{3:>10}")
# Currency formatting
price = 1234.567
print(f"Price: ${price:,.2f}")
# -----------------------------------------------------------------------------
# 8. String Formatting - Other Methods
# -----------------------------------------------------------------------------
# Alternative formatting methods
print("\n--- Other Formatting Methods ---")
# .format() method
name = "Alice"
age = 30
print("Hello, {}! You are {} years old.".format(name, age))
print("Hello, {0}! You are {1} years old.".format(name, age))
print("Hello, {n}! You are {a} years old.".format(n=name, a=age))
# % operator (older style)
print("Hello, %s! You are %d years old." % (name, age))
# -----------------------------------------------------------------------------
# 9. Escape Characters
# -----------------------------------------------------------------------------
# Special characters using backslash \
print("\n--- Escape Characters ---")
print("Line 1\nLine 2\nLine 3") # \n = newline
print("Column1\tColumn2\tColumn3") # \t = tab
print("She said: \"Hello!\"") # \" = quote
print('It\'s a beautiful day') # \' = apostrophe
print("Path: C:\\Users\\Documents") # \\ = backslash
print("Bell sound: \a") # \a = bell (may not work)
# Raw strings - ignore escape characters
print("\nRaw string:")
print(r"C:\Users\Baraa\Desktop") # r prefix for raw string
# -----------------------------------------------------------------------------
# 10. Multi-line Printing
# -----------------------------------------------------------------------------
print("\n--- Multi-line Strings ---")
# Using triple quotes
message = """
This is a multi-line message.
It spans across several lines.
Very useful for long text!
"""
print(message)
# ASCII art example
print("""
Welcome to Python!
Let's learn together! ║
""")
# -----------------------------------------------------------------------------
# 11. Practical Examples
# -----------------------------------------------------------------------------
print("--- Practical Examples ---")
# Receipt example
print("\n========== RECEIPT ==========")
item1, price1 = "Coffee", 4.99
item2, price2 = "Sandwich", 8.50
item3, price3 = "Cookie", 2.25
total = price1 + price2 + price3
print(f"{item1:.<20}${price1:.2f}")
print(f"{item2:.<20}${price2:.2f}")
print(f"{item3:.<20}${price3:.2f}")
print("=" * 30)
print(f"{'TOTAL':.<20}${total:.2f}")
# Table example
print("\n| Name | Age | City |")
print("|----------|-----|------------|")
print(f"| {'Alice':<8} | {25:<3} | {'New York':<10} |")
print(f"| {'Bob':<8} | {30:<3} | {'London':<10} |")
print(f"| {'Charlie':<8} | {35:<3} | {'Tokyo':<10} |")

343
01_basics/02_comments.py Normal file
View File

@ -0,0 +1,343 @@
"""
================================================================================
File: 02_comments.py
Topic: Comments in Python
================================================================================
This file demonstrates how to write comments in Python. Comments are essential
for code documentation, making your code readable, and helping others (and
your future self) understand what the code does.
Key Concepts:
- Single-line comments
- Multi-line comments
- Docstrings
- Best practices for commenting
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Single-Line Comments
# -----------------------------------------------------------------------------
# Use the hash symbol (#) to create single-line comments
print("--- Single-Line Comments ---")
# This is a single-line comment
print("Hello, World!") # This is an inline comment
# Comments can explain complex logic
x = 10 # Store the initial value
y = 20 # Store the second value
result = x + y # Calculate the sum
# Comments can be used to organize sections of code
# ---- Configuration ----
debug_mode = True
max_retries = 3
# ---- Main Logic ----
print(f"Debug mode is: {debug_mode}")
# -----------------------------------------------------------------------------
# 2. Multi-Line Comments
# -----------------------------------------------------------------------------
# Python doesn't have true multi-line comments, but there are two approaches
print("\n--- Multi-Line Comments ---")
# Approach 1: Multiple single-line comments (preferred)
# This is a multi-line comment
# that spans across multiple lines.
# Each line starts with a hash symbol.
# Approach 2: Triple-quoted strings (not recommended for comments)
# These are actually string literals, not true comments
"""
This looks like a multi-line comment, but it's actually
a string literal. If not assigned to a variable,
Python will ignore it, but it's still stored in memory.
Use this approach only for docstrings.
"""
print("Multi-line comments explained above!")
# -----------------------------------------------------------------------------
# 3. Docstrings (Documentation Strings)
# -----------------------------------------------------------------------------
# Docstrings are special strings used to document modules, functions, classes
print("\n--- Docstrings ---")
def greet(name):
"""
Greet a person by their name.
This function takes a person's name and prints a friendly
greeting message to the console.
Args:
name (str): The name of the person to greet.
Returns:
str: A greeting message.
Examples:
>>> greet("Baraa")
'Hello, Baraa! Welcome!'
"""
return f"Hello, {name}! Welcome!"
# Access the docstring
print(greet("Baraa"))
print(f"Function docstring: {greet.__doc__[:50]}...")
def calculate_area(length, width):
"""
Calculate the area of a rectangle.
Args:
length (float): The length of the rectangle.
width (float): The width of the rectangle.
Returns:
float: The area of the rectangle.
Raises:
ValueError: If length or width is negative.
"""
if length < 0 or width < 0:
raise ValueError("Length and width must be non-negative")
return length * width
print(f"Area: {calculate_area(5, 3)}")
# -----------------------------------------------------------------------------
# 4. Class Docstrings
# -----------------------------------------------------------------------------
print("\n--- Class Docstrings ---")
class Rectangle:
"""
A class to represent a rectangle.
This class provides methods to calculate the area and perimeter
of a rectangle, as well as other utility methods.
Attributes:
length (float): The length of the rectangle.
width (float): The width of the rectangle.
Methods:
area(): Returns the area of the rectangle.
perimeter(): Returns the perimeter of the rectangle.
"""
def __init__(self, length, width):
"""
Initialize a Rectangle instance.
Args:
length (float): The length of the rectangle.
width (float): The width of the rectangle.
"""
self.length = length
self.width = width
def area(self):
"""Calculate and return the area of the rectangle."""
return self.length * self.width
def perimeter(self):
"""Calculate and return the perimeter of the rectangle."""
return 2 * (self.length + self.width)
rect = Rectangle(10, 5)
print(f"Rectangle area: {rect.area()}")
print(f"Rectangle perimeter: {rect.perimeter()}")
# -----------------------------------------------------------------------------
# 5. Comment Best Practices
# -----------------------------------------------------------------------------
print("\n--- Comment Best Practices ---")
# ✅ GOOD: Explain WHY, not WHAT
# Calculate discount for loyalty members (15% off for 2+ years)
years_as_member = 3
discount = 0.15 if years_as_member >= 2 else 0
# ❌ BAD: Explains what the code obviously does
# x = x + 1 # Add 1 to x
# ✅ GOOD: Document complex algorithms
# Using binary search for O(log n) time complexity
# instead of linear search O(n) for performance
# ✅ GOOD: Mark TODO items for future work
# TODO: Implement caching for better performance
# FIXME: Handle edge case when input is empty
# NOTE: This function requires Python 3.8+
# HACK: Temporary workaround for API limitation
# ✅ GOOD: Use comments for code sections
# ============================================
# DATABASE CONNECTION SETUP
# ============================================
# ============================================
# USER AUTHENTICATION
# ============================================
print("Best practices demonstrated!")
# -----------------------------------------------------------------------------
# 6. Commenting Out Code
# -----------------------------------------------------------------------------
print("\n--- Commenting Out Code ---")
# You can temporarily disable code by commenting it out
# print("This line won't execute")
# old_function()
# deprecated_code()
# Useful during debugging
value = 100
# value = 200 # Uncomment to test with different value
print(f"Current value: {value}")
# Multiple lines can be commented at once
# line_1 = "first"
# line_2 = "second"
# line_3 = "third"
# -----------------------------------------------------------------------------
# 7. Module-Level Docstrings
# -----------------------------------------------------------------------------
print("\n--- Module-Level Docstrings ---")
# At the top of a Python file, you can include a module docstring
# (like the one at the top of this file)
# Access a module's docstring
print("This module's docstring starts with:")
print(__doc__[:100] + "...")
# -----------------------------------------------------------------------------
# 8. Type Hints with Comments
# -----------------------------------------------------------------------------
print("\n--- Type Hints with Comments ---")
def process_data(
data: list, # The input data to process
threshold: float = 0.5, # Minimum value to include (default: 0.5)
verbose: bool = False # Print progress if True
) -> list:
"""
Process a list of numerical data.
Args:
data: List of numbers to process.
threshold: Minimum value to include in results.
verbose: Whether to print processing details.
Returns:
Filtered list containing only values above threshold.
"""
if verbose:
print(f"Processing {len(data)} items...")
return [x for x in data if x > threshold]
result = process_data([0.1, 0.6, 0.3, 0.8, 0.4], threshold=0.5)
print(f"Filtered data: {result}")
# -----------------------------------------------------------------------------
# 9. Practical Example: Well-Commented Code
# -----------------------------------------------------------------------------
print("\n--- Practical Example ---")
def calculate_shipping_cost(weight, distance, express=False):
"""
Calculate the shipping cost based on weight and distance.
The cost is calculated using a base rate plus additional charges
for weight and distance. Express shipping doubles the final cost.
Args:
weight (float): Package weight in kilograms.
distance (float): Shipping distance in kilometers.
express (bool): Whether to use express shipping.
Returns:
float: Total shipping cost in dollars.
Example:
>>> calculate_shipping_cost(2.5, 100)
17.0
>>> calculate_shipping_cost(2.5, 100, express=True)
34.0
"""
# Base shipping rate
BASE_RATE = 5.00
# Rate per kilogram of weight
WEIGHT_RATE = 2.00
# Rate per 100 kilometers
DISTANCE_RATE = 0.05
# Calculate component costs
weight_cost = weight * WEIGHT_RATE
distance_cost = distance * DISTANCE_RATE
# Sum up total cost
total = BASE_RATE + weight_cost + distance_cost
# Apply express multiplier if applicable
if express:
total *= 2 # Express shipping is 2x the normal rate
return round(total, 2)
# Example usage
package_weight = 3.5 # kg
shipping_distance = 250 # km
standard_cost = calculate_shipping_cost(package_weight, shipping_distance)
express_cost = calculate_shipping_cost(package_weight, shipping_distance, express=True)
print(f"Standard shipping: ${standard_cost}")
print(f"Express shipping: ${express_cost}")
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("COMMENTS SUMMARY")
print("=" * 60)
print("""
1. Single-line: Use # for short comments
2. Multi-line: Use multiple # lines
3. Docstrings: Use triple quotes for documentation
4. Explain WHY, not WHAT
5. Keep comments up-to-date with code changes
6. Use TODO, FIXME, NOTE for special markers
7. Don't over-comment obvious code
""")

407
01_basics/03_variables.py Normal file
View File

@ -0,0 +1,407 @@
"""
================================================================================
File: 03_variables.py
Topic: Variables in Python
================================================================================
This file demonstrates how to create and use variables in Python. Variables
are containers for storing data values. Unlike other programming languages,
Python has no command for declaring a variable - it's created the moment
you assign a value to it.
Key Concepts:
- Variable creation and assignment
- Variable naming conventions
- Multiple assignments
- Variable types and type checking
- Constants
- Variable scope basics
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Creating Variables
# -----------------------------------------------------------------------------
# Variables are created when you assign a value to them
print("--- Creating Variables ---")
# Simple variable assignments
name = "Baraa"
age = 25
height = 1.75
is_student = True
print(f"Name: {name}")
print(f"Age: {age}")
print(f"Height: {height}m")
print(f"Is student: {is_student}")
# Variables can be reassigned to different values
x = 10
print(f"\nx = {x}")
x = 20
print(f"x = {x} (reassigned)")
# Variables can even change types (dynamic typing)
value = 100
print(f"\nvalue = {value} (type: {type(value).__name__})")
value = "one hundred"
print(f"value = {value} (type: {type(value).__name__})")
# -----------------------------------------------------------------------------
# 2. Variable Naming Rules
# -----------------------------------------------------------------------------
# Rules for Python variable names
print("\n--- Variable Naming Rules ---")
# ✅ Valid variable names
my_variable = 1 # Lowercase with underscores (snake_case) - RECOMMENDED
myVariable = 2 # camelCase - valid but not Pythonic
MyVariable = 3 # PascalCase - usually for classes
_private_var = 4 # Starts with underscore - convention for private
__very_private = 5 # Double underscore - name mangling
var123 = 6 # Can contain numbers (but not start with them)
CONSTANT = 7 # ALL CAPS - convention for constants
print(f"my_variable = {my_variable}")
print(f"_private_var = {_private_var}")
print(f"CONSTANT = {CONSTANT}")
# ❌ Invalid variable names (would cause errors):
# 123var = 1 # Cannot start with a number
# my-variable = 1 # Hyphens not allowed
# my variable = 1 # Spaces not allowed
# class = 1 # Cannot use reserved keywords
# Reserved keywords in Python (cannot use as variable names):
import keyword
print(f"\nPython reserved keywords: {keyword.kwlist[:10]}...")
# -----------------------------------------------------------------------------
# 3. Naming Conventions (PEP 8)
# -----------------------------------------------------------------------------
print("\n--- Naming Conventions (PEP 8) ---")
# Variables and functions: snake_case
user_name = "john_doe"
max_value = 100
is_active = True
# Constants: UPPER_SNAKE_CASE
PI = 3.14159
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
# Classes: PascalCase (CapWords)
# class MyClass:
# pass
# Private variables: leading underscore
_internal_state = "private"
# "Very private" / Name-mangled: double leading underscore
__name_mangled = "mangled"
print(f"user_name: {user_name}")
print(f"PI (constant): {PI}")
print(f"MAX_CONNECTIONS (constant): {MAX_CONNECTIONS}")
# Descriptive names are better than short names
# ✅ Good
total_price = 99.99
customer_name = "Alice"
is_logged_in = True
# ❌ Bad (avoid!)
# tp = 99.99
# cn = "Alice"
# il = True
# -----------------------------------------------------------------------------
# 4. Multiple Variable Assignment
# -----------------------------------------------------------------------------
print("\n--- Multiple Variable Assignment ---")
# Assign the same value to multiple variables
a = b = c = 100
print(f"a = {a}, b = {b}, c = {c}")
# Assign different values in one line
x, y, z = 1, 2, 3
print(f"x = {x}, y = {y}, z = {z}")
# Swap variables easily
print(f"\nBefore swap: x = {x}, y = {y}")
x, y = y, x
print(f"After swap: x = {x}, y = {y}")
# Unpack a list/tuple into variables
coordinates = (10, 20, 30)
x, y, z = coordinates
print(f"\nUnpacked coordinates: x={x}, y={y}, z={z}")
# Extended unpacking with *
first, *rest = [1, 2, 3, 4, 5]
print(f"first = {first}, rest = {rest}")
*beginning, last = [1, 2, 3, 4, 5]
print(f"beginning = {beginning}, last = {last}")
first, *middle, last = [1, 2, 3, 4, 5]
print(f"first = {first}, middle = {middle}, last = {last}")
# -----------------------------------------------------------------------------
# 5. Variable Types and Type Checking
# -----------------------------------------------------------------------------
print("\n--- Variable Types and Type Checking ---")
# Python is dynamically typed - types are determined at runtime
integer_var = 42
float_var = 3.14
string_var = "Hello"
bool_var = True
list_var = [1, 2, 3]
dict_var = {"key": "value"}
none_var = None
# Check types using type()
print(f"integer_var: {integer_var} -> {type(integer_var)}")
print(f"float_var: {float_var} -> {type(float_var)}")
print(f"string_var: {string_var} -> {type(string_var)}")
print(f"bool_var: {bool_var} -> {type(bool_var)}")
print(f"list_var: {list_var} -> {type(list_var)}")
print(f"dict_var: {dict_var} -> {type(dict_var)}")
print(f"none_var: {none_var} -> {type(none_var)}")
# Check if variable is of a specific type using isinstance()
print(f"\nIs integer_var an int? {isinstance(integer_var, int)}")
print(f"Is string_var a str? {isinstance(string_var, str)}")
print(f"Is list_var a list? {isinstance(list_var, list)}")
# Check multiple types
print(f"Is integer_var int or float? {isinstance(integer_var, (int, float))}")
# -----------------------------------------------------------------------------
# 6. Type Casting (Converting Types)
# -----------------------------------------------------------------------------
print("\n--- Type Casting ---")
# String to integer
str_num = "123"
int_num = int(str_num)
print(f"'{str_num}' (str) -> {int_num} (int)")
# String to float
str_float = "3.14"
float_num = float(str_float)
print(f"'{str_float}' (str) -> {float_num} (float)")
# Number to string
number = 42
str_number = str(number)
print(f"{number} (int) -> '{str_number}' (str)")
# Float to integer (truncates, doesn't round)
pi = 3.99
int_pi = int(pi)
print(f"{pi} (float) -> {int_pi} (int) [truncated]")
# Boolean conversions
print(f"\nbool(1) = {bool(1)}") # True
print(f"bool(0) = {bool(0)}") # False
print(f"bool('hello') = {bool('hello')}") # True
print(f"bool('') = {bool('')}") # False
print(f"bool([1, 2]) = {bool([1, 2])}") # True
print(f"bool([]) = {bool([])}") # False
# Integer to boolean
print(f"\nint(True) = {int(True)}") # 1
print(f"int(False) = {int(False)}") # 0
# -----------------------------------------------------------------------------
# 7. Constants
# -----------------------------------------------------------------------------
print("\n--- Constants ---")
# Python doesn't have true constants, but by convention:
# - Use UPPER_SNAKE_CASE for constants
# - Don't modify them after initial assignment
# Mathematical constants
PI = 3.14159265359
E = 2.71828182845
GOLDEN_RATIO = 1.61803398875
# Application constants
MAX_USERS = 1000
API_TIMEOUT = 30
BASE_URL = "https://api.example.com"
DEBUG_MODE = False
print(f"PI = {PI}")
print(f"MAX_USERS = {MAX_USERS}")
print(f"BASE_URL = {BASE_URL}")
# You CAN modify them (Python won't stop you), but you SHOULDN'T
# PI = 3 # Don't do this!
# For true constants, you can use:
# 1. Separate constants module (constants.py)
# 2. typing.Final (Python 3.8+)
from typing import Final
MAX_SIZE: Final = 100
# MAX_SIZE = 200 # Type checker will warn about this
print(f"MAX_SIZE (Final) = {MAX_SIZE}")
# -----------------------------------------------------------------------------
# 8. Variable Scope (Basic)
# -----------------------------------------------------------------------------
print("\n--- Variable Scope ---")
# Global variable
global_var = "I'm global"
def my_function():
# Local variable
local_var = "I'm local"
print(f"Inside function - global_var: {global_var}")
print(f"Inside function - local_var: {local_var}")
my_function()
print(f"Outside function - global_var: {global_var}")
# print(local_var) # Error! local_var doesn't exist here
# Modifying global variables inside functions
counter = 0
def increment():
global counter # Declare we want to use global counter
counter += 1
print(f"\nBefore increment: counter = {counter}")
increment()
increment()
print(f"After 2 increments: counter = {counter}")
# -----------------------------------------------------------------------------
# 9. Variable Identity and Memory
# -----------------------------------------------------------------------------
print("\n--- Variable Identity and Memory ---")
# id() returns the memory address of a variable
a = 10
b = 10
c = a
print(f"a = {a}, id(a) = {id(a)}")
print(f"b = {b}, id(b) = {id(b)}")
print(f"c = {c}, id(c) = {id(c)}")
# Small integers (-5 to 256) are cached in Python
print(f"\na is b: {a is b}") # True (same memory location)
print(f"a is c: {a is c}") # True
# Larger numbers may have different ids
x = 1000
y = 1000
print(f"\nx = {x}, id(x) = {id(x)}")
print(f"y = {y}, id(y) = {id(y)}")
# 'is' vs '=='
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(f"\nlist1 == list2: {list1 == list2}") # True (same values)
print(f"list1 is list2: {list1 is list2}") # False (different objects)
print(f"list1 is list3: {list1 is list3}") # True (same object)
# -----------------------------------------------------------------------------
# 10. Deleting Variables
# -----------------------------------------------------------------------------
print("\n--- Deleting Variables ---")
temp_var = "I will be deleted"
print(f"temp_var exists: {temp_var}")
del temp_var
# print(temp_var) # Error! NameError: name 'temp_var' is not defined
print("temp_var has been deleted")
# Check if variable exists
if 'temp_var' not in dir():
print("temp_var no longer exists in current scope")
# -----------------------------------------------------------------------------
# 11. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Example 1: User profile
first_name = "Baraa"
last_name = "Shaer"
full_name = f"{first_name} {last_name}"
email = f"{first_name.lower()}.{last_name.lower()}@example.com"
print(f"Full Name: {full_name}")
print(f"Email: {email}")
# Example 2: Shopping cart
item_price = 29.99
quantity = 3
discount_percent = 10
subtotal = item_price * quantity
discount_amount = subtotal * (discount_percent / 100)
total = subtotal - discount_amount
print(f"\nSubtotal: ${subtotal:.2f}")
print(f"Discount ({discount_percent}%): -${discount_amount:.2f}")
print(f"Total: ${total:.2f}")
# Example 3: Temperature conversion
celsius = 25
fahrenheit = (celsius * 9/5) + 32
kelvin = celsius + 273.15
print(f"\n{celsius}°C = {fahrenheit}°F = {kelvin}K")
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("VARIABLES SUMMARY")
print("=" * 60)
print("""
1. Variables are created by assignment (=)
2. Python is dynamically typed
3. Use snake_case for variables (PEP 8)
4. Use UPPER_CASE for constants
5. Multiple assignments: x, y, z = 1, 2, 3
6. Check types with type() or isinstance()
7. Convert types with int(), str(), float(), bool()
8. Variables have scope (local vs global)
9. Use 'is' for identity, '==' for equality
10. Delete variables with 'del'
""")

536
01_basics/04_data_types.py Normal file
View File

@ -0,0 +1,536 @@
"""
================================================================================
File: 04_data_types.py
Topic: Data Types in Python
================================================================================
This file demonstrates Python's built-in data types. Understanding data types
is fundamental to programming as they determine what operations can be
performed on data and how it's stored in memory.
Key Concepts:
- Numeric types (int, float, complex)
- Text type (str)
- Boolean type (bool)
- Sequence types (list, tuple, range)
- Mapping type (dict)
- Set types (set, frozenset)
- None type
- Type checking and conversion
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Numeric Types Overview
# -----------------------------------------------------------------------------
print("=" * 60)
print("NUMERIC TYPES")
print("=" * 60)
# -----------------------------------------------------------------------------
# 1.1 Integers (int)
# -----------------------------------------------------------------------------
print("\n--- Integers (int) ---")
# Basic integers
positive_int = 42
negative_int = -17
zero = 0
print(f"Positive: {positive_int}")
print(f"Negative: {negative_int}")
print(f"Zero: {zero}")
# Large integers (Python handles arbitrary precision)
big_number = 123456789012345678901234567890
print(f"Big number: {big_number}")
print(f"Type: {type(big_number)}")
# Different number bases
binary = 0b1010 # Binary (base 2)
octal = 0o17 # Octal (base 8)
hexadecimal = 0xFF # Hexadecimal (base 16)
print(f"\nBinary 0b1010 = {binary}")
print(f"Octal 0o17 = {octal}")
print(f"Hex 0xFF = {hexadecimal}")
# Underscores for readability (Python 3.6+)
million = 1_000_000
credit_card = 1234_5678_9012_3456
print(f"\nMillion: {million}")
print(f"Credit card: {credit_card}")
# Integer operations
a, b = 17, 5
print(f"\na = {a}, b = {b}")
print(f"a + b = {a + b}") # Addition
print(f"a - b = {a - b}") # Subtraction
print(f"a * b = {a * b}") # Multiplication
print(f"a / b = {a / b}") # Division (returns float)
print(f"a // b = {a // b}") # Floor division
print(f"a % b = {a % b}") # Modulo (remainder)
print(f"a ** b = {a ** b}") # Exponentiation
# -----------------------------------------------------------------------------
# 1.2 Floating-Point Numbers (float)
# -----------------------------------------------------------------------------
print("\n--- Floating-Point Numbers (float) ---")
# Basic floats
pi = 3.14159
negative_float = -2.5
scientific = 2.5e10 # Scientific notation (2.5 × 10^10)
print(f"Pi: {pi}")
print(f"Negative: {negative_float}")
print(f"Scientific 2.5e10: {scientific}")
print(f"Type: {type(pi)}")
# Float precision limitations
print(f"\n0.1 + 0.2 = {0.1 + 0.2}") # Not exactly 0.3!
print(f"0.1 + 0.2 == 0.3: {0.1 + 0.2 == 0.3}") # False!
# For precise decimal calculations, use the decimal module
from decimal import Decimal
d1 = Decimal('0.1')
d2 = Decimal('0.2')
print(f"Decimal: {d1} + {d2} = {d1 + d2}")
# Special float values
infinity = float('inf')
neg_infinity = float('-inf')
not_a_number = float('nan')
print(f"\nInfinity: {infinity}")
print(f"Negative infinity: {neg_infinity}")
print(f"NaN: {not_a_number}")
print(f"1000 < infinity: {1000 < infinity}")
# Float methods
f = 3.7
print(f"\n{f}.is_integer(): {f.is_integer()}")
print(f"4.0.is_integer(): {(4.0).is_integer()}")
# -----------------------------------------------------------------------------
# 1.3 Complex Numbers (complex)
# -----------------------------------------------------------------------------
print("\n--- Complex Numbers (complex) ---")
# Creating complex numbers
c1 = 3 + 4j
c2 = complex(2, -1)
print(f"c1 = {c1}")
print(f"c2 = {c2}")
print(f"Type: {type(c1)}")
# Accessing parts
print(f"\nc1.real = {c1.real}")
print(f"c1.imag = {c1.imag}")
print(f"Conjugate of c1: {c1.conjugate()}")
# Complex arithmetic
print(f"\nc1 + c2 = {c1 + c2}")
print(f"c1 * c2 = {c1 * c2}")
print(f"abs(c1) = {abs(c1)}") # Magnitude
# -----------------------------------------------------------------------------
# 2. Text Type (str)
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("TEXT TYPE - STRINGS")
print("=" * 60)
# Creating strings
single_quotes = 'Hello, World!'
double_quotes = "Python is awesome"
multi_line = """This is a
multi-line string"""
print(f"Single quotes: {single_quotes}")
print(f"Double quotes: {double_quotes}")
print(f"Multi-line:\n{multi_line}")
# String operations
s = "Hello, Python!"
print(f"\nString: '{s}'")
print(f"Length: {len(s)}")
print(f"Uppercase: {s.upper()}")
print(f"Lowercase: {s.lower()}")
print(f"Title case: {s.title()}")
print(f"Replace: {s.replace('Python', 'World')}")
print(f"Split: {s.split(', ')}")
print(f"Strip: {' hello '.strip()}")
# String indexing and slicing
print(f"\nIndexing: s[0] = '{s[0]}', s[-1] = '{s[-1]}'")
print(f"Slicing: s[0:5] = '{s[0:5]}'")
print(f"Step: s[::2] = '{s[::2]}'")
print(f"Reverse: s[::-1] = '{s[::-1]}'")
# String methods for checking
text = "Python3"
print(f"\n'{text}'.isalnum(): {text.isalnum()}")
print(f"'python'.isalpha(): {'python'.isalpha()}")
print(f"'12345'.isdigit(): {'12345'.isdigit()}")
print(f"'hello'.islower(): {'hello'.islower()}")
print(f"'HELLO'.isupper(): {'HELLO'.isupper()}")
# String formatting
name = "Baraa"
age = 25
print(f"\nf-string: {name} is {age} years old")
print("format(): {} is {} years old".format(name, age))
print("%-formatting: %s is %d years old" % (name, age))
# Escape characters
print(f"\nNewline: Hello\\nWorld → Hello\nWorld")
print(f"Tab: Hello\\tWorld → Hello\tWorld")
print("Quote: She said \"Hi!\"")
# Raw strings
print(f"\nRaw string: r'C:\\Users\\Name'{r'C:\Users\Name'}")
# -----------------------------------------------------------------------------
# 3. Boolean Type (bool)
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("BOOLEAN TYPE")
print("=" * 60)
# Boolean values
is_active = True
is_deleted = False
print(f"is_active: {is_active}, type: {type(is_active)}")
print(f"is_deleted: {is_deleted}")
# Boolean as integers
print(f"\nTrue as int: {int(True)}") # 1
print(f"False as int: {int(False)}") # 0
print(f"True + True = {True + True}") # 2
# Comparison operators return booleans
x, y = 10, 5
print(f"\nx = {x}, y = {y}")
print(f"x > y: {x > y}")
print(f"x < y: {x < y}")
print(f"x == y: {x == y}")
print(f"x != y: {x != y}")
print(f"x >= y: {x >= y}")
# Logical operators
print(f"\nTrue and False: {True and False}")
print(f"True or False: {True or False}")
print(f"not True: {not True}")
# Truthy and Falsy values
print("\nFalsy values (evaluate to False):")
falsy_values = [False, 0, 0.0, "", [], {}, set(), None]
for val in falsy_values:
print(f" bool({repr(val)}) = {bool(val)}")
print("\nTruthy values (evaluate to True):")
truthy_values = [True, 1, -1, 3.14, "hello", [1, 2], {"a": 1}]
for val in truthy_values:
print(f" bool({repr(val)}) = {bool(val)}")
# -----------------------------------------------------------------------------
# 4. Sequence Types
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("SEQUENCE TYPES")
print("=" * 60)
# -----------------------------------------------------------------------------
# 4.1 Lists
# -----------------------------------------------------------------------------
print("\n--- Lists (Mutable Sequences) ---")
# Creating lists
empty_list = []
numbers = [1, 2, 3, 4, 5]
mixed = [1, "two", 3.0, True, None]
nested = [[1, 2], [3, 4], [5, 6]]
print(f"Numbers: {numbers}")
print(f"Mixed types: {mixed}")
print(f"Nested: {nested}")
# List operations
fruits = ["apple", "banana", "cherry"]
print(f"\nFruits: {fruits}")
print(f"Length: {len(fruits)}")
print(f"First: {fruits[0]}, Last: {fruits[-1]}")
print(f"Slice: {fruits[1:3]}")
# Modifying lists
fruits.append("date")
print(f"After append: {fruits}")
fruits.insert(1, "blueberry")
print(f"After insert at 1: {fruits}")
fruits.remove("banana")
print(f"After remove 'banana': {fruits}")
popped = fruits.pop()
print(f"Popped: {popped}, List: {fruits}")
# List comprehension
squares = [x**2 for x in range(1, 6)]
print(f"\nSquares (comprehension): {squares}")
# -----------------------------------------------------------------------------
# 4.2 Tuples
# -----------------------------------------------------------------------------
print("\n--- Tuples (Immutable Sequences) ---")
# Creating tuples
empty_tuple = ()
single = (1,) # Note the comma
coordinates = (10, 20, 30)
mixed_tuple = (1, "two", 3.0)
print(f"Coordinates: {coordinates}")
print(f"Type: {type(coordinates)}")
# Tuple unpacking
x, y, z = coordinates
print(f"Unpacked: x={x}, y={y}, z={z}")
# Tuples are immutable
# coordinates[0] = 100 # This would raise an error
# Named tuples
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
print(f"\nNamed tuple: {p}")
print(f"p.x = {p.x}, p.y = {p.y}")
# -----------------------------------------------------------------------------
# 4.3 Range
# -----------------------------------------------------------------------------
print("\n--- Range ---")
# Creating ranges
r1 = range(5) # 0-4
r2 = range(1, 6) # 1-5
r3 = range(0, 10, 2) # 0, 2, 4, 6, 8
print(f"range(5): {list(r1)}")
print(f"range(1, 6): {list(r2)}")
print(f"range(0, 10, 2): {list(r3)}")
print(f"Type: {type(r1)}")
# Range is memory efficient
big_range = range(1000000)
print(f"\nrange(1000000) - Length: {len(big_range)}")
print(f"500000 in big_range: {500000 in big_range}")
# -----------------------------------------------------------------------------
# 5. Mapping Type (dict)
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("MAPPING TYPE - DICTIONARY")
print("=" * 60)
# Creating dictionaries
empty_dict = {}
person = {"name": "Baraa", "age": 25, "city": "Gaza"}
using_dict = dict(a=1, b=2, c=3)
print(f"Person: {person}")
print(f"Type: {type(person)}")
# Accessing values
print(f"\nName: {person['name']}")
print(f"Age (get): {person.get('age')}")
print(f"Country (get with default): {person.get('country', 'Unknown')}")
# Modifying dictionaries
person["email"] = "baraa@example.com" # Add
person["age"] = 26 # Update
print(f"\nAfter modifications: {person}")
# Dictionary methods
print(f"\nKeys: {list(person.keys())}")
print(f"Values: {list(person.values())}")
print(f"Items: {list(person.items())}")
# Dictionary comprehension
squares_dict = {x: x**2 for x in range(1, 6)}
print(f"\nSquares dict: {squares_dict}")
# -----------------------------------------------------------------------------
# 6. Set Types
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("SET TYPES")
print("=" * 60)
# Creating sets (mutable, unordered, unique elements)
empty_set = set() # Note: {} creates an empty dict, not set
numbers_set = {1, 2, 3, 4, 5}
from_list = set([1, 2, 2, 3, 3, 3]) # Duplicates removed
print(f"Numbers set: {numbers_set}")
print(f"From list with duplicates: {from_list}")
print(f"Type: {type(numbers_set)}")
# Set operations
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(f"\nSet A: {a}")
print(f"Set B: {b}")
print(f"Union (A | B): {a | b}")
print(f"Intersection (A & B): {a & b}")
print(f"Difference (A - B): {a - b}")
print(f"Symmetric difference (A ^ B): {a ^ b}")
# Set methods
s = {1, 2, 3}
s.add(4)
print(f"\nAfter add(4): {s}")
s.discard(2)
print(f"After discard(2): {s}")
# Frozenset (immutable set)
frozen = frozenset([1, 2, 3])
print(f"\nFrozenset: {frozen}")
# frozen.add(4) # This would raise an error
# Sets are useful for membership testing
valid_statuses = {"active", "pending", "completed"}
user_status = "active"
print(f"\n'{user_status}' is valid: {user_status in valid_statuses}")
# -----------------------------------------------------------------------------
# 7. None Type
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("NONE TYPE")
print("=" * 60)
# None represents absence of a value
result = None
print(f"result = {result}")
print(f"Type: {type(result)}")
print(f"result is None: {result is None}") # Use 'is' not '=='
# Common uses of None
def greet(name=None):
"""Function with optional parameter"""
if name is None:
return "Hello, Guest!"
return f"Hello, {name}!"
print(f"\ngreet(): {greet()}")
print(f"greet('Baraa'): {greet('Baraa')}")
# None as placeholder
data = None
# ... later in code ...
data = fetch_data() if False else [] # Simulated
print(f"Data initialized: {data}")
# -----------------------------------------------------------------------------
# 8. Type Checking and Conversion
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("TYPE CHECKING AND CONVERSION")
print("=" * 60)
# Using type()
values = [42, 3.14, "hello", True, [1, 2], {"a": 1}, None]
print("Type checking with type():")
for val in values:
print(f" {repr(val):15} -> {type(val).__name__}")
# Using isinstance() (preferred for type checking)
print("\nType checking with isinstance():")
x = 42
print(f"isinstance(42, int): {isinstance(x, int)}")
print(f"isinstance(42, (int, float)): {isinstance(x, (int, float))}")
# Type conversion summary
print("\nType Conversion Examples:")
print(f"int('42') = {int('42')}")
print(f"float('3.14') = {float('3.14')}")
print(f"str(42) = {str(42)}")
print(f"bool(1) = {bool(1)}")
print(f"list('abc') = {list('abc')}")
print(f"tuple([1,2,3]) = {tuple([1, 2, 3])}")
print(f"set([1,2,2,3]) = {set([1, 2, 2, 3])}")
print(f"dict([('a',1),('b',2)]) = {dict([('a', 1), ('b', 2)])}")
# -----------------------------------------------------------------------------
# 9. Data Type Comparison Table
# -----------------------------------------------------------------------------
print("\n" + "=" * 60)
print("DATA TYPE COMPARISON TABLE")
print("=" * 60)
print("""
| Type | Mutable | Ordered | Duplicates | Example |
|-----------|---------|---------|------------|----------------------|
| int | No | N/A | N/A | 42 |
| float | No | N/A | N/A | 3.14 |
| complex | No | N/A | N/A | 3+4j |
| str | No | Yes | Yes | "hello" |
| bool | No | N/A | N/A | True |
| list | Yes | Yes | Yes | [1, 2, 3] |
| tuple | No | Yes | Yes | (1, 2, 3) |
| range | No | Yes | No | range(5) |
| dict | Yes | Yes* | Keys: No | {"a": 1} |
| set | Yes | No | No | {1, 2, 3} |
| frozenset | No | No | No | frozenset({1, 2, 3}) |
| NoneType | No | N/A | N/A | None |
* Dicts preserve insertion order in Python 3.7+
""")
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
print("=" * 60)
print("DATA TYPES SUMMARY")
print("=" * 60)
print("""
Numeric: int, float, complex
Text: str (immutable)
Boolean: bool (True/False)
Sequences: list (mutable), tuple (immutable), range
Mapping: dict (key-value pairs)
Sets: set (mutable), frozenset (immutable)
Special: None (absence of value)
Key Points:
1. Python is dynamically typed
2. Use type() or isinstance() to check types
3. Use appropriate type for your data
4. Mutable types can be changed, immutable cannot
5. Choose between list/tuple based on mutability needs
6. Use dict for key-value mappings
7. Use set for unique elements and fast membership tests
""")

Binary file not shown.

View File

@ -0,0 +1,145 @@
"""
================================================================================
File: 01_if_else.py
Topic: Conditional Statements - if/else
================================================================================
This file demonstrates the fundamentals of conditional statements in Python.
Conditional statements allow your program to make decisions and execute
different code blocks based on whether conditions are True or False.
Key Concepts:
- if statement: Executes code block if condition is True
- else statement: Executes code block if condition is False
- Comparison operators: ==, !=, <, >, <=, >=
- Logical operators: and, or, not
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Simple if Statement
# -----------------------------------------------------------------------------
# The most basic form - executes code only if condition is True
age = 18
if age >= 18:
print("You are an adult.")
# -----------------------------------------------------------------------------
# 2. if-else Statement
# -----------------------------------------------------------------------------
# Provides an alternative when the condition is False
temperature = 15
if temperature > 25:
print("It's a hot day!")
else:
print("It's not that hot today.")
# -----------------------------------------------------------------------------
# 3. Comparison Operators
# -----------------------------------------------------------------------------
# Used to compare values and return True or False
x = 10
y = 20
print("\n--- Comparison Operators ---")
print(f"x == y: {x == y}") # Equal to
print(f"x != y: {x != y}") # Not equal to
print(f"x < y: {x < y}") # Less than
print(f"x > y: {x > y}") # Greater than
print(f"x <= y: {x <= y}") # Less than or equal to
print(f"x >= y: {x >= y}") # Greater than or equal to
# -----------------------------------------------------------------------------
# 4. Logical Operators (and, or, not)
# -----------------------------------------------------------------------------
# Combine multiple conditions
age = 25
has_license = True
print("\n--- Logical Operators ---")
# 'and' - Both conditions must be True
if age >= 18 and has_license:
print("You can drive a car.")
# 'or' - At least one condition must be True
is_weekend = True
is_holiday = False
if is_weekend or is_holiday:
print("You can relax today!")
# 'not' - Reverses the boolean value
is_raining = False
if not is_raining:
print("You don't need an umbrella.")
# -----------------------------------------------------------------------------
# 5. Nested if Statements
# -----------------------------------------------------------------------------
# You can place if statements inside other if statements
print("\n--- Nested if Statements ---")
score = 85
attendance = 90
if score >= 60:
print("You passed the exam!")
if attendance >= 80:
print("And you have excellent attendance!")
else:
print("But try to improve your attendance.")
else:
print("You need to study harder.")
# -----------------------------------------------------------------------------
# 6. Truthy and Falsy Values
# -----------------------------------------------------------------------------
# In Python, some values are considered False: 0, "", [], {}, None, False
# Everything else is considered True
print("\n--- Truthy and Falsy Values ---")
empty_list = []
full_list = [1, 2, 3]
if full_list:
print("The list has items.")
if not empty_list:
print("The list is empty.")
# -----------------------------------------------------------------------------
# 7. Ternary Operator (One-line if-else)
# -----------------------------------------------------------------------------
# A compact way to write simple if-else statements
age = 20
status = "adult" if age >= 18 else "minor"
print(f"\nTernary result: You are an {status}.")
# -----------------------------------------------------------------------------
# 8. Practical Example: Simple Login Check
# -----------------------------------------------------------------------------
print("\n--- Practical Example: Login Check ---")
username = "admin"
password = "secret123"
input_user = "admin"
input_pass = "secret123"
if input_user == username and input_pass == password:
print("Login successful! Welcome back.")
else:
print("Invalid username or password.")

213
02_control_flow/02_elif.py Normal file
View File

@ -0,0 +1,213 @@
"""
================================================================================
File: 02_elif.py
Topic: Multiple Conditions with elif
================================================================================
This file demonstrates how to handle multiple conditions using 'elif' (else if).
When you have more than two possible outcomes, elif allows you to check
multiple conditions in sequence.
Key Concepts:
- elif chains for multiple conditions
- Order matters - first True condition wins
- Default case with else
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic elif Structure
# -----------------------------------------------------------------------------
# Check multiple conditions in sequence
score = 85
print("--- Grade Calculator ---")
if score >= 90:
grade = "A"
print("Excellent work!")
elif score >= 80:
grade = "B"
print("Good job!")
elif score >= 70:
grade = "C"
print("Satisfactory.")
elif score >= 60:
grade = "D"
print("You passed, but need improvement.")
else:
grade = "F"
print("You need to study harder.")
print(f"Your grade: {grade}")
# -----------------------------------------------------------------------------
# 2. Order Matters in elif
# -----------------------------------------------------------------------------
# The first condition that evaluates to True will be executed
print("\n--- Order Matters Example ---")
number = 15
# Correct order (most specific first)
if number > 20:
print("Greater than 20")
elif number > 10:
print("Greater than 10") # This will execute
elif number > 5:
print("Greater than 5")
else:
print("5 or less")
# -----------------------------------------------------------------------------
# 3. Day of the Week Example
# -----------------------------------------------------------------------------
# Classic example showing multiple discrete options
print("\n--- Day of the Week ---")
day = 3 # 1=Monday, 2=Tuesday, etc.
if day == 1:
day_name = "Monday"
elif day == 2:
day_name = "Tuesday"
elif day == 3:
day_name = "Wednesday"
elif day == 4:
day_name = "Thursday"
elif day == 5:
day_name = "Friday"
elif day == 6:
day_name = "Saturday"
elif day == 7:
day_name = "Sunday"
else:
day_name = "Invalid day"
print(f"Day {day} is {day_name}")
# -----------------------------------------------------------------------------
# 4. Temperature Categories
# -----------------------------------------------------------------------------
# Real-world example with ranges
print("\n--- Temperature Categories ---")
celsius = 22
if celsius < 0:
category = "Freezing"
advice = "Stay indoors if possible!"
elif celsius < 10:
category = "Cold"
advice = "Wear a warm jacket."
elif celsius < 20:
category = "Cool"
advice = "A light jacket is recommended."
elif celsius < 30:
category = "Warm"
advice = "Perfect weather for outdoor activities."
else:
category = "Hot"
advice = "Stay hydrated!"
print(f"Temperature: {celsius}°C")
print(f"Category: {category}")
print(f"Advice: {advice}")
# -----------------------------------------------------------------------------
# 5. BMI Calculator Example
# -----------------------------------------------------------------------------
# Practical health-related example
print("\n--- BMI Calculator ---")
weight = 70 # kg
height = 1.75 # meters
bmi = weight / (height ** 2)
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal weight"
elif bmi < 30:
category = "Overweight"
else:
category = "Obese"
print(f"BMI: {bmi:.1f}")
print(f"Category: {category}")
# -----------------------------------------------------------------------------
# 6. Age Group Classification
# -----------------------------------------------------------------------------
# Using elif for demographic categories
print("\n--- Age Group Classification ---")
age = 25
if age < 0:
group = "Invalid age"
elif age < 13:
group = "Child"
elif age < 20:
group = "Teenager"
elif age < 30:
group = "Young Adult"
elif age < 60:
group = "Adult"
else:
group = "Senior"
print(f"Age {age} belongs to: {group}")
# -----------------------------------------------------------------------------
# 7. Combined Conditions with elif
# -----------------------------------------------------------------------------
# Using logical operators within elif
print("\n--- Combined Conditions ---")
hour = 14 # 24-hour format
is_weekend = False
if hour < 6:
greeting = "It's very early!"
elif hour < 12 and not is_weekend:
greeting = "Good morning, time to work!"
elif hour < 12 and is_weekend:
greeting = "Good morning, enjoy your day off!"
elif hour < 18:
greeting = "Good afternoon!"
elif hour < 22:
greeting = "Good evening!"
else:
greeting = "Good night!"
print(f"Hour: {hour}:00 - {greeting}")
# -----------------------------------------------------------------------------
# 8. Handling User Input Example
# -----------------------------------------------------------------------------
print("\n--- Menu Selection Example ---")
# Simulating user choice (in real code, you'd use input())
choice = "B"
if choice == "A" or choice == "a":
print("You selected: Start Game")
elif choice == "B" or choice == "b":
print("You selected: Load Game")
elif choice == "C" or choice == "c":
print("You selected: Settings")
elif choice == "Q" or choice == "q":
print("Goodbye!")
else:
print("Invalid choice. Please try again.")

View File

@ -0,0 +1,243 @@
"""
================================================================================
File: 03_match_case.py
Topic: Pattern Matching with match-case (Python 3.10+)
================================================================================
This file demonstrates Python's structural pattern matching introduced in
Python 3.10. It's similar to switch/case in other languages but much more
powerful with pattern matching capabilities.
Key Concepts:
- Basic match-case syntax
- The underscore (_) as wildcard/default case
- Pattern matching with guards
- Matching sequences and mappings
- Capturing values in patterns
Note: This requires Python 3.10 or later!
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic match-case (Similar to switch/case)
# -----------------------------------------------------------------------------
# The simplest form of pattern matching
def get_day_name(day_number: int) -> str:
"""Convert day number to day name."""
match day_number:
case 1:
return "Monday"
case 2:
return "Tuesday"
case 3:
return "Wednesday"
case 4:
return "Thursday"
case 5:
return "Friday"
case 6:
return "Saturday"
case 7:
return "Sunday"
case _: # Default case (underscore is wildcard)
return "Invalid day"
print("--- Basic match-case ---")
print(f"Day 1: {get_day_name(1)}")
print(f"Day 5: {get_day_name(5)}")
print(f"Day 9: {get_day_name(9)}")
# -----------------------------------------------------------------------------
# 2. Multiple Patterns in One Case
# -----------------------------------------------------------------------------
# Use the | operator to match multiple values
def categorize_day(day: str) -> str:
"""Categorize day as weekday or weekend."""
match day.lower():
case "saturday" | "sunday":
return "Weekend - Time to relax!"
case "monday" | "tuesday" | "wednesday" | "thursday" | "friday":
return "Weekday - Time to work!"
case _:
return "Unknown day"
print("\n--- Multiple Patterns ---")
print(f"Saturday: {categorize_day('Saturday')}")
print(f"Monday: {categorize_day('Monday')}")
# -----------------------------------------------------------------------------
# 3. Pattern Matching with Guards
# -----------------------------------------------------------------------------
# Add conditions using 'if' after the pattern
def evaluate_score(score: int) -> str:
"""Evaluate score with guards."""
match score:
case n if n < 0:
return "Invalid score (negative)"
case n if n > 100:
return "Invalid score (over 100)"
case n if n >= 90:
return f"Grade A - Excellent! ({n}%)"
case n if n >= 80:
return f"Grade B - Good! ({n}%)"
case n if n >= 70:
return f"Grade C - Satisfactory ({n}%)"
case n if n >= 60:
return f"Grade D - Pass ({n}%)"
case _:
return f"Grade F - Fail ({score}%)"
print("\n--- Pattern with Guards ---")
print(evaluate_score(95))
print(evaluate_score(75))
print(evaluate_score(45))
print(evaluate_score(-10))
# -----------------------------------------------------------------------------
# 4. Matching Sequences (Tuples/Lists)
# -----------------------------------------------------------------------------
# Match against list or tuple patterns
def describe_point(point):
"""Describe a point based on its coordinates."""
match point:
case (0, 0):
return "Origin"
case (0, y):
return f"On Y-axis at y={y}"
case (x, 0):
return f"On X-axis at x={x}"
case (x, y) if x == y:
return f"On diagonal at ({x}, {y})"
case (x, y):
return f"Point at ({x}, {y})"
case _:
return "Not a valid point"
print("\n--- Matching Sequences ---")
print(describe_point((0, 0)))
print(describe_point((0, 5)))
print(describe_point((3, 0)))
print(describe_point((4, 4)))
print(describe_point((3, 7)))
# -----------------------------------------------------------------------------
# 5. Matching with Variable Length Sequences
# -----------------------------------------------------------------------------
# Use * to capture remaining elements
def analyze_list(data):
"""Analyze list structure."""
match data:
case []:
return "Empty list"
case [single]:
return f"Single element: {single}"
case [first, second]:
return f"Two elements: {first} and {second}"
case [first, *middle, last]:
return f"First: {first}, Last: {last}, Middle count: {len(middle)}"
case _:
return "Not a list"
print("\n--- Variable Length Matching ---")
print(analyze_list([]))
print(analyze_list([42]))
print(analyze_list([1, 2]))
print(analyze_list([1, 2, 3, 4, 5]))
# -----------------------------------------------------------------------------
# 6. Matching Dictionaries
# -----------------------------------------------------------------------------
# Match against dictionary patterns
def process_request(request):
"""Process different types of requests."""
match request:
case {"type": "login", "user": username}:
return f"User '{username}' is logging in"
case {"type": "logout", "user": username}:
return f"User '{username}' is logging out"
case {"type": "message", "user": username, "content": content}:
return f"Message from '{username}': {content}"
case {"type": action}:
return f"Unknown action: {action}"
case _:
return "Invalid request format"
print("\n--- Matching Dictionaries ---")
print(process_request({"type": "login", "user": "Baraa"}))
print(process_request({"type": "message", "user": "Ali", "content": "Hello!"}))
print(process_request({"type": "logout", "user": "Sara"}))
print(process_request({"type": "unknown_action"}))
# -----------------------------------------------------------------------------
# 7. Matching Class Instances
# -----------------------------------------------------------------------------
# Match against object attributes
class Point:
"""Simple point class for demonstration."""
def __init__(self, x, y):
self.x = x
self.y = y
def classify_point(point):
"""Classify point using class matching."""
match point:
case Point(x=0, y=0):
return "At origin"
case Point(x=0, y=y):
return f"On Y-axis at {y}"
case Point(x=x, y=0):
return f"On X-axis at {x}"
case Point(x=x, y=y):
return f"At ({x}, {y})"
case _:
return "Not a Point"
print("\n--- Matching Class Instances ---")
print(classify_point(Point(0, 0)))
print(classify_point(Point(0, 7)))
print(classify_point(Point(5, 0)))
print(classify_point(Point(3, 4)))
# -----------------------------------------------------------------------------
# 8. Practical Example: Command Parser
# -----------------------------------------------------------------------------
# Real-world use case for a simple command interpreter
def execute_command(command):
"""Parse and execute simple commands."""
parts = command.split()
match parts:
case ["quit"] | ["exit"] | ["q"]:
return "Goodbye!"
case ["hello"]:
return "Hello there!"
case ["hello", name]:
return f"Hello, {name}!"
case ["add", x, y]:
return f"Result: {int(x) + int(y)}"
case ["repeat", count, *words]:
return " ".join(words) * int(count)
case ["help"]:
return "Available: quit, hello [name], add x y, repeat n words..."
case [unknown, *_]:
return f"Unknown command: {unknown}. Type 'help' for assistance."
case _:
return "Please enter a command"
print("\n--- Command Parser Example ---")
print(f"'hello': {execute_command('hello')}")
print(f"'hello Baraa': {execute_command('hello Baraa')}")
print(f"'add 5 3': {execute_command('add 5 3')}")
print(f"'repeat 3 Hi': {execute_command('repeat 3 Hi ')}")
print(f"'help': {execute_command('help')}")
print(f"'xyz': {execute_command('xyz')}")

228
03_loops/01_for_loop.py Normal file
View File

@ -0,0 +1,228 @@
"""
================================================================================
File: 01_for_loop.py
Topic: For Loops in Python
================================================================================
This file demonstrates the 'for' loop in Python, which is used to iterate
over sequences (lists, tuples, strings, ranges, etc.) or any iterable object.
Key Concepts:
- Basic for loop syntax
- range() function for numeric iterations
- Iterating over different data types
- enumerate() for index and value
- zip() for parallel iteration
- Nested for loops
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic For Loop
# -----------------------------------------------------------------------------
# Iterate over items in a sequence
print("--- Basic For Loop ---")
fruits = ["apple", "banana", "cherry", "date"]
for fruit in fruits:
print(f"I like {fruit}")
# -----------------------------------------------------------------------------
# 2. Using range() Function
# -----------------------------------------------------------------------------
# range(start, stop, step) - generates numbers from start to stop-1
print("\n--- Using range() ---")
# range(5) generates 0, 1, 2, 3, 4
print("range(5):")
for i in range(5):
print(i, end=" ")
print()
# range(2, 8) generates 2, 3, 4, 5, 6, 7
print("\nrange(2, 8):")
for i in range(2, 8):
print(i, end=" ")
print()
# range(0, 10, 2) generates 0, 2, 4, 6, 8 (step of 2)
print("\nrange(0, 10, 2):")
for i in range(0, 10, 2):
print(i, end=" ")
print()
# Counting backwards: range(10, 0, -1)
print("\nrange(10, 0, -1) - Countdown:")
for i in range(10, 0, -1):
print(i, end=" ")
print("Blastoff!")
# -----------------------------------------------------------------------------
# 3. Iterating Over Strings
# -----------------------------------------------------------------------------
# Each character in a string is an item
print("\n--- Iterating Over Strings ---")
word = "Python"
print(f"Characters in '{word}':")
for char in word:
print(f" {char}")
# -----------------------------------------------------------------------------
# 4. Using enumerate() - Get Index and Value
# -----------------------------------------------------------------------------
# enumerate() provides both the index and the item
print("\n--- Using enumerate() ---")
languages = ["Python", "JavaScript", "C++", "Rust"]
for index, language in enumerate(languages):
print(f"{index + 1}. {language}")
# Start from a custom number
print("\nWith custom start:")
for rank, language in enumerate(languages, start=1):
print(f"Rank {rank}: {language}")
# -----------------------------------------------------------------------------
# 5. Using zip() - Parallel Iteration
# -----------------------------------------------------------------------------
# zip() combines multiple iterables and iterates over them together
print("\n--- Using zip() ---")
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
cities = ["New York", "London", "Tokyo"]
# Iterate over multiple lists simultaneously
for name, age, city in zip(names, ages, cities):
print(f"{name} is {age} years old and lives in {city}")
# -----------------------------------------------------------------------------
# 6. Iterating Over Dictionaries
# -----------------------------------------------------------------------------
# Different ways to loop through dictionary data
print("\n--- Iterating Over Dictionaries ---")
person = {
"name": "Baraa",
"age": 25,
"city": "Gaza",
"profession": "Developer"
}
# Keys only (default)
print("Keys:")
for key in person:
print(f" {key}")
# Values only
print("\nValues:")
for value in person.values():
print(f" {value}")
# Both keys and values
print("\nKey-Value pairs:")
for key, value in person.items():
print(f" {key}: {value}")
# -----------------------------------------------------------------------------
# 7. Nested For Loops
# -----------------------------------------------------------------------------
# A loop inside another loop
print("\n--- Nested For Loops ---")
# Multiplication table
print("Multiplication Table (1-5):")
for i in range(1, 6):
for j in range(1, 6):
print(f"{i * j:3}", end=" ")
print() # New line after each row
# Working with 2D lists (matrices)
print("\n2D Matrix:")
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
for row in matrix:
for cell in row:
print(f"{cell}", end=" ")
print()
# -----------------------------------------------------------------------------
# 8. Else Clause in For Loop
# -----------------------------------------------------------------------------
# The else block executes when the loop completes without break
print("\n--- For-Else Clause ---")
# Search for a number
numbers = [1, 3, 5, 7, 9]
search_for = 5
for num in numbers:
if num == search_for:
print(f"Found {search_for}!")
break
else:
print(f"{search_for} was not found.")
# -----------------------------------------------------------------------------
# 9. List Comprehension (Related to For Loops)
# -----------------------------------------------------------------------------
# A concise way to create lists using for loops
print("\n--- List Comprehension Preview ---")
# Traditional way
squares_traditional = []
for x in range(1, 6):
squares_traditional.append(x ** 2)
print(f"Traditional: {squares_traditional}")
# List comprehension way
squares_comprehension = [x ** 2 for x in range(1, 6)]
print(f"Comprehension: {squares_comprehension}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Sum of numbers
numbers = [10, 20, 30, 40, 50]
total = 0
for num in numbers:
total += num
print(f"Sum of {numbers} = {total}")
# Finding maximum
values = [23, 45, 12, 78, 34]
maximum = values[0]
for value in values:
if value > maximum:
maximum = value
print(f"Maximum in {values} = {maximum}")
# Counting vowels in a string
text = "Hello, World!"
vowels = "aeiouAEIOU"
vowel_count = 0
for char in text:
if char in vowels:
vowel_count += 1
print(f"Vowels in '{text}': {vowel_count}")

263
03_loops/02_while_loop.py Normal file
View File

@ -0,0 +1,263 @@
"""
================================================================================
File: 02_while_loop.py
Topic: While Loops in Python
================================================================================
This file demonstrates the 'while' loop in Python, which repeatedly executes
a block of code as long as a condition remains True. Unlike for loops,
while loops are typically used when the number of iterations is not known
in advance.
Key Concepts:
- Basic while loop syntax
- Counter-controlled loops
- Sentinel-controlled loops (input validation)
- Infinite loops and how to handle them
- While-else construct
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic While Loop
# -----------------------------------------------------------------------------
# Executes as long as condition is True
print("--- Basic While Loop ---")
count = 1
while count <= 5:
print(f"Count: {count}")
count += 1 # Don't forget to update the condition variable!
print("Loop finished!")
# -----------------------------------------------------------------------------
# 2. Counter-Controlled While Loop
# -----------------------------------------------------------------------------
# When you know how many iterations you need
print("\n--- Counter-Controlled Loop ---")
# Countdown
countdown = 5
print("Rocket Launch Countdown:")
while countdown > 0:
print(f"T-minus {countdown}...")
countdown -= 1
print("Liftoff! 🚀")
# -----------------------------------------------------------------------------
# 3. Summing Numbers with While
# -----------------------------------------------------------------------------
# Accumulator pattern
print("\n--- Sum of Numbers ---")
# Sum numbers from 1 to 10
n = 1
total = 0
while n <= 10:
total += n
n += 1
print(f"Sum of 1 to 10 = {total}")
# -----------------------------------------------------------------------------
# 4. Sentinel-Controlled Loop (Input Validation)
# -----------------------------------------------------------------------------
# Loop until a specific condition is met
print("\n--- Sentinel-Controlled Loop ---")
# Simulating password validation (in practice, use input())
attempts = 0
max_attempts = 3
correct_password = "secret123"
# Simulated user inputs
user_inputs = ["wrong1", "wrong2", "secret123"]
while attempts < max_attempts:
# In real code: password = input("Enter password: ")
password = user_inputs[attempts]
print(f"Attempt {attempts + 1}: Entered '{password}'")
if password == correct_password:
print("Access granted! ✓")
break
else:
print("Incorrect password.")
attempts += 1
else:
# This runs if the loop completes without break
print("Too many failed attempts. Account locked.")
# -----------------------------------------------------------------------------
# 5. While Loop with User Simulation
# -----------------------------------------------------------------------------
# Menu-driven program example
print("\n--- Menu-Driven Example ---")
# Simulated menu choices
menu_selections = [1, 2, 4, 3]
selection_index = 0
running = True
while running:
# Simulating menu selection
choice = menu_selections[selection_index]
selection_index += 1
print(f"\nMenu: 1=Add, 2=View, 3=Exit | Choice: {choice}")
if choice == 1:
print(" ➤ Adding item...")
elif choice == 2:
print(" ➤ Viewing items...")
elif choice == 3:
print(" ➤ Exiting program...")
running = False
else:
print(" ➤ Invalid choice, try again.")
# -----------------------------------------------------------------------------
# 6. Infinite Loops (and how to avoid them)
# -----------------------------------------------------------------------------
# A loop that never ends - usually a bug
print("\n--- Avoiding Infinite Loops ---")
# BAD: This would run forever (commented out)
# i = 0
# while i < 5:
# print(i)
# # Missing: i += 1
# GOOD: Always update the condition variable
i = 0
while i < 5:
print(f"i = {i}")
i += 1 # This is crucial!
# -----------------------------------------------------------------------------
# 7. While-Else Construct
# -----------------------------------------------------------------------------
# The else block executes if the loop completes normally (no break)
print("\n--- While-Else Construct ---")
# Example 1: Loop completes normally
n = 5
while n > 0:
print(n, end=" ")
n -= 1
else:
print("\n ↳ Countdown complete! (else block executed)")
# Example 2: Loop exits via break
print("\nSearching in list:")
items = ["a", "b", "c", "d"]
target = "b"
index = 0
while index < len(items):
if items[index] == target:
print(f" Found '{target}' at index {index}")
break
index += 1
else:
print(f" '{target}' not found")
# -----------------------------------------------------------------------------
# 8. Nested While Loops
# -----------------------------------------------------------------------------
# A while inside another while
print("\n--- Nested While Loops ---")
# Create a simple pattern
row = 1
while row <= 4:
col = 1
while col <= row:
print("*", end=" ")
col += 1
print() # New line
row += 1
# -----------------------------------------------------------------------------
# 9. While with Break and Continue
# -----------------------------------------------------------------------------
# Control flow within loops (preview - detailed in next file)
print("\n--- Break and Continue in While ---")
# Break example
print("Break example (stop at 5):")
i = 0
while True: # Intentional infinite loop
i += 1
if i > 5:
break
print(i, end=" ")
print()
# Continue example
print("\nContinue example (skip even numbers):")
i = 0
while i < 10:
i += 1
if i % 2 == 0:
continue # Skip even numbers
print(i, end=" ")
print()
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Fibonacci sequence
print("Fibonacci sequence (first 10 numbers):")
a, b = 0, 1
count = 0
while count < 10:
print(a, end=" ")
a, b = b, a + b
count += 1
print()
# Finding digits in a number
print("\nDigits of 12345:")
number = 12345
while number > 0:
digit = number % 10
print(f" Digit: {digit}")
number //= 10
# Guessing game logic
print("\n Guess the Number Game Logic:")
secret = 7
guesses = [3, 8, 5, 7]
guess_index = 0
while guess_index < len(guesses):
guess = guesses[guess_index]
print(f" Guess: {guess}", end=" ")
if guess < secret:
print("- Too low!")
elif guess > secret:
print("- Too high!")
else:
print("- Correct! 🎉")
break
guess_index += 1

View File

@ -0,0 +1,286 @@
"""
================================================================================
File: 03_break_continue.py
Topic: Loop Control Statements - break, continue, pass
================================================================================
This file demonstrates loop control statements that alter the normal flow
of loop execution. These are essential tools for writing efficient and
readable loops.
Key Concepts:
- break: Exit the loop immediately
- continue: Skip to the next iteration
- pass: Do nothing (placeholder)
- Practical use cases for each
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. The break Statement
# -----------------------------------------------------------------------------
# Immediately exits the loop, skipping any remaining iterations
print("--- The break Statement ---")
# Example 1: Exit when target is found
print("Finding first even number:")
numbers = [1, 3, 5, 8, 9, 10]
for num in numbers:
print(f" Checking {num}...", end=" ")
if num % 2 == 0:
print(f"Found! {num} is even.")
break
print("Odd, continuing...")
# Example 2: Exit on specific condition
print("\nSearching for name 'Charlie':")
names = ["Alice", "Bob", "Charlie", "David", "Eve"]
for name in names:
if name == "Charlie":
print(f" ✓ Found '{name}'!")
break
print(f" Checking '{name}'...")
# -----------------------------------------------------------------------------
# 2. The continue Statement
# -----------------------------------------------------------------------------
# Skips the rest of the current iteration and moves to the next
print("\n--- The continue Statement ---")
# Example 1: Skip negative numbers
print("Processing only positive numbers:")
values = [5, -2, 8, -1, 10, -3, 7]
for value in values:
if value < 0:
print(f" Skipping {value} (negative)")
continue
print(f" Processing {value}")
# Example 2: Skip specific items
print("\nPrinting all fruits except banana:")
fruits = ["apple", "banana", "cherry", "banana", "date"]
for fruit in fruits:
if fruit == "banana":
continue
print(f" {fruit}")
# -----------------------------------------------------------------------------
# 3. The pass Statement
# -----------------------------------------------------------------------------
# Does nothing - used as a placeholder
print("\n--- The pass Statement ---")
# Example 1: Placeholder in empty function/class
class FutureFeature:
pass # Will implement later
def not_implemented_yet():
pass # Placeholder for future code
# Example 2: Explicit "do nothing" in conditionals
print("Processing numbers (ignoring zeros for now):")
numbers = [1, 0, 2, 0, 3]
for num in numbers:
if num == 0:
pass # Explicitly do nothing (could be a TODO)
else:
print(f" Number: {num}")
# -----------------------------------------------------------------------------
# 4. break vs continue - Side by Side Comparison
# -----------------------------------------------------------------------------
print("\n--- break vs continue Comparison ---")
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# With break - stops at 5
print("With break (stop at 5):")
for num in data:
if num == 5:
break
print(num, end=" ")
print()
# With continue - skips 5
print("With continue (skip 5):")
for num in data:
if num == 5:
continue
print(num, end=" ")
print()
# -----------------------------------------------------------------------------
# 5. break with while Loops
# -----------------------------------------------------------------------------
print("\n--- break with while Loops ---")
# Breaking out of an infinite loop
print("Processing until 'quit' command:")
commands = ["start", "process", "load", "quit", "save"]
index = 0
while True:
command = commands[index]
print(f" Command: {command}")
if command == "quit":
print(" ↳ Exiting loop...")
break
index += 1
# -----------------------------------------------------------------------------
# 6. continue with while Loops
# -----------------------------------------------------------------------------
print("\n--- continue with while Loops ---")
# Skip multiples of 3
print("Numbers 1-10 (skipping multiples of 3):")
i = 0
while i < 10:
i += 1
if i % 3 == 0:
continue
print(i, end=" ")
print()
# -----------------------------------------------------------------------------
# 7. Nested Loops with break
# -----------------------------------------------------------------------------
# break only exits the innermost loop
print("\n--- Nested Loops with break ---")
print("Breaking inner loop only:")
for i in range(1, 4):
print(f" Outer loop: i = {i}")
for j in range(1, 4):
if j == 2:
print(" ↳ Breaking inner loop at j = 2")
break
print(f" Inner loop: j = {j}")
# Using a flag to break outer loop
print("\nBreaking outer loop with flag:")
stop_outer = False
for i in range(1, 4):
if stop_outer:
break
for j in range(1, 4):
print(f" i={i}, j={j}")
if i == 2 and j == 2:
print(" ↳ Breaking both loops!")
stop_outer = True
break
# -----------------------------------------------------------------------------
# 8. for-else with break
# -----------------------------------------------------------------------------
# else block runs only if loop completes without break
print("\n--- for-else with break ---")
# Example: Searching for a prime number
def is_prime(n):
"""Check if n is prime."""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False # Found a divisor, not prime
return True # No divisors found, is prime
# Search for first prime in list
numbers = [4, 6, 8, 9, 11, 12]
print(f"Searching for prime in {numbers}:")
for num in numbers:
if is_prime(num):
print(f" → First prime found: {num}")
break
else:
print(" → No primes found in the list")
# -----------------------------------------------------------------------------
# 9. Practical Example: Input Validation
# -----------------------------------------------------------------------------
print("\n--- Practical Example: Input Validation ---")
# Simulating user input validation
test_inputs = ["abc", "-5", "150", "42"]
print("Validating inputs (must be 1-100):")
for input_str in test_inputs:
print(f"\n Input: '{input_str}'")
# Check if it's a number
if not input_str.lstrip('-').isdigit():
print(" ✗ Error: Not a valid number")
continue
value = int(input_str)
# Check range
if value < 1:
print(" ✗ Error: Too low (must be >= 1)")
continue
if value > 100:
print(" ✗ Error: Too high (must be <= 100)")
continue
print(f" ✓ Valid input: {value}")
break
else:
print("\n No valid input found!")
# -----------------------------------------------------------------------------
# 10. Practical Example: Skip Processing on Error
# -----------------------------------------------------------------------------
print("\n--- Practical Example: Error Handling ---")
# Processing a list of files (simulated)
files = [
{"name": "data1.csv", "readable": True},
{"name": "data2.csv", "readable": False}, # Error
{"name": "data3.csv", "readable": True},
{"name": "corrupt.csv", "readable": True, "corrupt": True}, # Error
{"name": "data4.csv", "readable": True},
]
print("Processing files:")
processed_count = 0
for file in files:
name = file["name"]
# Skip unreadable files
if not file.get("readable", False):
print(f"{name}: Cannot read file, skipping...")
continue
# Skip corrupt files
if file.get("corrupt", False):
print(f"{name}: File is corrupt, skipping...")
continue
# Process the file
print(f"{name}: Processing complete")
processed_count += 1
print(f"\nTotal files processed: {processed_count}/{len(files)}")

View File

@ -0,0 +1,287 @@
"""
================================================================================
File: 01_lists.py
Topic: Python Lists - Ordered, Mutable Collections
================================================================================
This file demonstrates Python lists, which are ordered, mutable collections
that can store elements of any type. Lists are one of the most versatile
and commonly used data structures in Python.
Key Concepts:
- Creating and accessing lists
- List methods (append, insert, remove, pop, etc.)
- Slicing and indexing
- List operations (concatenation, repetition)
- Nested lists
- List comprehensions
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Creating Lists
# -----------------------------------------------------------------------------
# Lists are created with square brackets []
print("--- Creating Lists ---")
# Empty list
empty_list = []
print(f"Empty list: {empty_list}")
# List with elements
numbers = [1, 2, 3, 4, 5]
fruits = ["apple", "banana", "cherry"]
mixed = [1, "hello", 3.14, True, None]
print(f"Numbers: {numbers}")
print(f"Fruits: {fruits}")
print(f"Mixed types: {mixed}")
# Using list() constructor
chars = list("Python")
print(f"From string: {chars}")
# List from range
range_list = list(range(1, 6))
print(f"From range: {range_list}")
# -----------------------------------------------------------------------------
# 2. Accessing Elements (Indexing)
# -----------------------------------------------------------------------------
# Lists are zero-indexed; negative indices count from the end
print("\n--- Indexing ---")
colors = ["red", "green", "blue", "yellow", "purple"]
print(f"List: {colors}")
print(f"First element (index 0): {colors[0]}")
print(f"Third element (index 2): {colors[2]}")
print(f"Last element (index -1): {colors[-1]}")
print(f"Second to last (index -2): {colors[-2]}")
# -----------------------------------------------------------------------------
# 3. Slicing Lists
# -----------------------------------------------------------------------------
# Syntax: list[start:stop:step]
print("\n--- Slicing ---")
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(f"Original: {nums}")
print(f"nums[2:5]: {nums[2:5]}") # Elements 2, 3, 4
print(f"nums[:4]: {nums[:4]}") # First 4 elements
print(f"nums[6:]: {nums[6:]}") # From index 6 to end
print(f"nums[::2]: {nums[::2]}") # Every 2nd element
print(f"nums[::-1]: {nums[::-1]}") # Reversed list
print(f"nums[1:8:2]: {nums[1:8:2]}") # Odd indices from 1 to 7
# -----------------------------------------------------------------------------
# 4. Modifying Lists
# -----------------------------------------------------------------------------
# Lists are mutable - you can change their contents
print("\n--- Modifying Lists ---")
languages = ["Python", "Java", "C++"]
print(f"Original: {languages}")
# Change single element
languages[1] = "JavaScript"
print(f"After changing index 1: {languages}")
# Change multiple elements with slicing
languages[0:2] = ["Rust", "Go"]
print(f"After slice replacement: {languages}")
# -----------------------------------------------------------------------------
# 5. List Methods
# -----------------------------------------------------------------------------
# Built-in methods to manipulate lists
print("\n--- List Methods ---")
# append() - Add element to end
items = [1, 2, 3]
items.append(4)
print(f"After append(4): {items}")
# extend() - Add multiple elements
items.extend([5, 6])
print(f"After extend([5, 6]): {items}")
# insert() - Add element at specific position
items.insert(0, 0) # Insert 0 at index 0
print(f"After insert(0, 0): {items}")
# remove() - Remove first occurrence of value
items.remove(3)
print(f"After remove(3): {items}")
# pop() - Remove and return element at index (default: last)
popped = items.pop()
print(f"Popped: {popped}, List now: {items}")
popped = items.pop(0)
print(f"Popped at 0: {popped}, List now: {items}")
# index() - Find index of first occurrence
fruits = ["apple", "banana", "cherry", "banana"]
print(f"\nfruits = {fruits}")
print(f"Index of 'banana': {fruits.index('banana')}")
# count() - Count occurrences
print(f"Count of 'banana': {fruits.count('banana')}")
# sort() - Sort in place
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
numbers.sort()
print(f"\nAfter sort(): {numbers}")
numbers.sort(reverse=True)
print(f"After sort(reverse=True): {numbers}")
# reverse() - Reverse in place
numbers.reverse()
print(f"After reverse(): {numbers}")
# copy() - Create a shallow copy
original = [1, 2, 3]
copied = original.copy()
print(f"\nOriginal: {original}, Copy: {copied}")
# clear() - Remove all elements
copied.clear()
print(f"After clear(): {copied}")
# -----------------------------------------------------------------------------
# 6. List Operations
# -----------------------------------------------------------------------------
print("\n--- List Operations ---")
# Concatenation (+)
list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined = list1 + list2
print(f"{list1} + {list2} = {combined}")
# Repetition (*)
repeated = [0] * 5
print(f"[0] * 5 = {repeated}")
# Membership (in)
fruits = ["apple", "banana", "cherry"]
print(f"'banana' in fruits: {'banana' in fruits}")
print(f"'grape' in fruits: {'grape' in fruits}")
# Length
print(f"len(fruits): {len(fruits)}")
# Min and Max
numbers = [5, 2, 8, 1, 9]
print(f"min({numbers}): {min(numbers)}")
print(f"max({numbers}): {max(numbers)}")
print(f"sum({numbers}): {sum(numbers)}")
# -----------------------------------------------------------------------------
# 7. Nested Lists (2D Lists)
# -----------------------------------------------------------------------------
print("\n--- Nested Lists ---")
# Creating a 2D list (matrix)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print("Matrix:")
for row in matrix:
print(f" {row}")
# Accessing elements
print(f"\nElement at [1][2]: {matrix[1][2]}") # Row 1, Column 2 = 6
print(f"Second row: {matrix[1]}")
# Modifying nested element
matrix[0][0] = 100
print(f"After matrix[0][0] = 100: {matrix[0]}")
# -----------------------------------------------------------------------------
# 8. List Comprehensions
# -----------------------------------------------------------------------------
# Concise way to create lists
print("\n--- List Comprehensions ---")
# Basic list comprehension
squares = [x**2 for x in range(1, 6)]
print(f"Squares 1-5: {squares}")
# With condition
even_squares = [x**2 for x in range(1, 11) if x % 2 == 0]
print(f"Even squares: {even_squares}")
# With expression
words = ["hello", "world", "python"]
upper_words = [word.upper() for word in words]
print(f"Uppercase: {upper_words}")
# Nested comprehension (flattening)
matrix = [[1, 2], [3, 4], [5, 6]]
flattened = [num for row in matrix for num in row]
print(f"Flattened: {flattened}")
# -----------------------------------------------------------------------------
# 9. Copying Lists - Shallow vs Deep
# -----------------------------------------------------------------------------
print("\n--- Copying Lists ---")
import copy
# Shallow copy - nested objects share reference
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0][0] = 999 # Affects both!
print(f"Shallow copy issue:")
print(f" Original: {original}")
print(f" Shallow: {shallow}")
# Deep copy - completely independent
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 999 # Only affects copy
print(f"\nDeep copy:")
print(f" Original: {original}")
print(f" Deep: {deep}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Finding unique elements while preserving order
items = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = []
for item in items:
if item not in unique:
unique.append(item)
print(f"Unique elements: {unique}")
# Filtering with list comprehension
scores = [85, 42, 91, 78, 55, 99, 66]
passing = [score for score in scores if score >= 60]
print(f"Passing scores: {passing}")
# Transforming data
temperatures_c = [0, 10, 20, 30, 40]
temperatures_f = [(c * 9/5) + 32 for c in temperatures_c]
print(f"Celsius: {temperatures_c}")
print(f"Fahrenheit: {temperatures_f}")

View File

@ -0,0 +1,286 @@
"""
================================================================================
File: 02_tuples.py
Topic: Python Tuples - Ordered, Immutable Collections
================================================================================
This file demonstrates Python tuples, which are ordered, immutable collections.
Unlike lists, tuples cannot be modified after creation, making them useful
for data that should not change.
Key Concepts:
- Creating tuples
- Tuple immutability
- Accessing elements
- Tuple unpacking
- When to use tuples vs lists
- Named tuples for clarity
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Creating Tuples
# -----------------------------------------------------------------------------
# Tuples are created with parentheses () or just commas
print("--- Creating Tuples ---")
# Empty tuple
empty_tuple = ()
print(f"Empty tuple: {empty_tuple}")
# Tuple with elements
coordinates = (10, 20)
colors = ("red", "green", "blue")
mixed = (1, "hello", 3.14, True)
print(f"Coordinates: {coordinates}")
print(f"Colors: {colors}")
print(f"Mixed types: {mixed}")
# Single element tuple - needs trailing comma!
single = (42,) # This is a tuple
not_tuple = (42) # This is just an integer!
print(f"\nSingle element tuple: {single}, type: {type(single)}")
print(f"Without comma: {not_tuple}, type: {type(not_tuple)}")
# Tuple without parentheses (packing)
packed = 1, 2, 3
print(f"Packed tuple: {packed}, type: {type(packed)}")
# Using tuple() constructor
from_list = tuple([1, 2, 3])
from_string = tuple("Python")
print(f"From list: {from_list}")
print(f"From string: {from_string}")
# -----------------------------------------------------------------------------
# 2. Accessing Elements
# -----------------------------------------------------------------------------
# Same indexing and slicing as lists
print("\n--- Accessing Elements ---")
fruits = ("apple", "banana", "cherry", "date", "elderberry")
print(f"Tuple: {fruits}")
print(f"First element: {fruits[0]}")
print(f"Last element: {fruits[-1]}")
print(f"Slice [1:4]: {fruits[1:4]}")
print(f"Every other: {fruits[::2]}")
# -----------------------------------------------------------------------------
# 3. Tuple Immutability
# -----------------------------------------------------------------------------
# Tuples cannot be modified after creation
print("\n--- Immutability ---")
point = (10, 20, 30)
print(f"Original point: {point}")
# This would raise an error:
# point[0] = 100 # TypeError: 'tuple' object does not support item assignment
# However, you can create a new tuple
point = (100,) + point[1:] # Create new tuple
print(f"New point: {point}")
# Note: Mutable objects inside tuples can still be modified
mutable_inside = ([1, 2], [3, 4])
mutable_inside[0].append(3) # This works!
print(f"Mutable inside tuple: {mutable_inside}")
# -----------------------------------------------------------------------------
# 4. Tuple Unpacking
# -----------------------------------------------------------------------------
# Assign tuple elements to multiple variables
print("\n--- Tuple Unpacking ---")
# Basic unpacking
coordinates = (100, 200, 300)
x, y, z = coordinates
print(f"Unpacked: x={x}, y={y}, z={z}")
# Swap values using tuple unpacking
a, b = 1, 2
print(f"Before swap: a={a}, b={b}")
a, b = b, a # Elegant swap!
print(f"After swap: a={a}, b={b}")
# Unpacking with * (extended unpacking)
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers
print(f"\nExtended unpacking:")
print(f" first: {first}")
print(f" middle: {middle}") # This becomes a list!
print(f" last: {last}")
# Ignoring values with underscore
data = ("John", 25, "Developer", "NYC")
name, _, job, _ = data # Ignore age and city
print(f"Name: {name}, Job: {job}")
# -----------------------------------------------------------------------------
# 5. Tuple Methods
# -----------------------------------------------------------------------------
# Tuples have only two methods (because they're immutable)
print("\n--- Tuple Methods ---")
numbers = (1, 2, 3, 2, 4, 2, 5)
print(f"Tuple: {numbers}")
# count() - Count occurrences
print(f"Count of 2: {numbers.count(2)}")
# index() - Find index of first occurrence
print(f"Index of 4: {numbers.index(4)}")
# -----------------------------------------------------------------------------
# 6. Tuple Operations
# -----------------------------------------------------------------------------
print("\n--- Tuple Operations ---")
# Concatenation
t1 = (1, 2, 3)
t2 = (4, 5, 6)
combined = t1 + t2
print(f"{t1} + {t2} = {combined}")
# Repetition
repeated = (0,) * 5
print(f"(0,) * 5 = {repeated}")
# Membership
colors = ("red", "green", "blue")
print(f"'green' in colors: {'green' in colors}")
# Length
print(f"len(colors): {len(colors)}")
# Min, Max, Sum
nums = (5, 2, 8, 1, 9)
print(f"min: {min(nums)}, max: {max(nums)}, sum: {sum(nums)}")
# -----------------------------------------------------------------------------
# 7. Tuples as Dictionary Keys
# -----------------------------------------------------------------------------
# Tuples can be used as dictionary keys (lists cannot)
print("\n--- Tuples as Dictionary Keys ---")
# Mapping (x, y) coordinates to location names
locations = {
(40.7128, -74.0060): "New York City",
(34.0522, -118.2437): "Los Angeles",
(51.5074, -0.1278): "London"
}
print("Locations dictionary:")
for coords, city in locations.items():
print(f" {coords}: {city}")
# Access by tuple key
coord = (40.7128, -74.0060)
print(f"\nLocation at {coord}: {locations[coord]}")
# -----------------------------------------------------------------------------
# 8. Returning Multiple Values from Functions
# -----------------------------------------------------------------------------
# Functions can return tuples
print("\n--- Functions Returning Tuples ---")
def get_stats(numbers):
"""Return multiple statistics as a tuple."""
return min(numbers), max(numbers), sum(numbers) / len(numbers)
data = [10, 20, 30, 40, 50]
minimum, maximum, average = get_stats(data)
print(f"Stats for {data}:")
print(f" Min: {minimum}, Max: {maximum}, Avg: {average}")
def divide_with_remainder(a, b):
"""Return quotient and remainder."""
return a // b, a % b
quotient, remainder = divide_with_remainder(17, 5)
print(f"\n17 ÷ 5 = {quotient} remainder {remainder}")
# -----------------------------------------------------------------------------
# 9. Named Tuples
# -----------------------------------------------------------------------------
# Tuples with named fields for clarity
print("\n--- Named Tuples ---")
from collections import namedtuple
# Define a named tuple type
Person = namedtuple("Person", ["name", "age", "city"])
# Create instances
person1 = Person("Baraa", 25, "Gaza")
person2 = Person(name="Sara", age=30, city="Cairo")
print(f"Person 1: {person1}")
print(f"Person 2: {person2}")
# Access by name or index
print(f"\nAccess by name: {person1.name}")
print(f"Access by index: {person1[0]}")
# Named tuple is still immutable
# person1.age = 26 # This would raise an error
# Convert to dictionary
print(f"As dict: {person1._asdict()}")
# Create from dictionary
data = {"name": "Ali", "age": 28, "city": "Dubai"}
person3 = Person(**data)
print(f"From dict: {person3}")
# -----------------------------------------------------------------------------
# 10. When to Use Tuples vs Lists
# -----------------------------------------------------------------------------
print("\n--- Tuples vs Lists ---")
# Use TUPLES when:
# 1. Data should not change
rgb_red = (255, 0, 0) # Color values shouldn't change
# 2. Using as dictionary keys
grid_positions = {(0, 0): "start", (9, 9): "end"}
# 3. Returning multiple values from functions
def get_point(): return (10, 20)
# 4. Heterogeneous data (different types, specific meaning)
person = ("John", 30, "Engineer") # name, age, job
# Use LISTS when:
# 1. Data will be modified
shopping_cart = ["apple", "bread"] # Items will be added/removed
# 2. Homogeneous collection
scores = [85, 90, 78, 92] # All same type, will be processed together
# 3. Order matters and you need to sort/shuffle
rankings = [3, 1, 4, 1, 5]
rankings.sort() # Lists are sortable in place
print("Summary:")
print(" Tuples: Immutable, hashable, for fixed data")
print(" Lists: Mutable, flexible, for changing data")
# Performance note: Tuples are slightly faster and use less memory
import sys
list_size = sys.getsizeof([1, 2, 3, 4, 5])
tuple_size = sys.getsizeof((1, 2, 3, 4, 5))
print(f"\nMemory: List={list_size} bytes, Tuple={tuple_size} bytes")

View File

@ -0,0 +1,299 @@
"""
================================================================================
File: 03_sets.py
Topic: Python Sets - Unordered Collections of Unique Elements
================================================================================
This file demonstrates Python sets, which are unordered collections that
store only unique elements. Sets are highly optimized for membership testing
and mathematical set operations.
Key Concepts:
- Creating sets
- Adding and removing elements
- Set operations (union, intersection, difference)
- Frozen sets (immutable sets)
- Use cases for sets
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Creating Sets
# -----------------------------------------------------------------------------
# Sets are created with curly braces {} or set()
print("--- Creating Sets ---")
# Using curly braces
fruits = {"apple", "banana", "cherry"}
print(f"Fruits set: {fruits}")
# Duplicates are automatically removed
numbers = {1, 2, 2, 3, 3, 3, 4}
print(f"Numbers (duplicates removed): {numbers}")
# Empty set - must use set(), not {}
empty_set = set() # {} creates an empty dictionary!
print(f"Empty set: {empty_set}, type: {type(empty_set)}")
print(f"Empty dict: {{}}, type: {type({})}")
# From other iterables
from_list = set([1, 2, 3, 4, 5])
from_string = set("hello") # Unique characters
from_tuple = set((10, 20, 30))
print(f"\nFrom list: {from_list}")
print(f"From string 'hello': {from_string}")
print(f"From tuple: {from_tuple}")
# -----------------------------------------------------------------------------
# 2. Set Characteristics
# -----------------------------------------------------------------------------
# Unordered, no duplicates, no indexing
print("\n--- Set Characteristics ---")
colors = {"red", "green", "blue"}
# Sets are unordered - no indexing
# colors[0] # This would raise an error!
# Check membership (very fast - O(1))
print(f"'red' in colors: {'red' in colors}")
print(f"'yellow' in colors: {'yellow' in colors}")
# Length
print(f"Number of colors: {len(colors)}")
# Sets can only contain immutable (hashable) elements
valid_set = {1, "hello", (1, 2), 3.14} # OK
# invalid_set = {1, [2, 3]} # Error! Lists are not hashable
# -----------------------------------------------------------------------------
# 3. Adding and Removing Elements
# -----------------------------------------------------------------------------
print("\n--- Adding and Removing Elements ---")
languages = {"Python", "Java"}
print(f"Initial: {languages}")
# add() - Add single element
languages.add("JavaScript")
print(f"After add('JavaScript'): {languages}")
# Adding duplicate has no effect
languages.add("Python")
print(f"After add('Python'): {languages}")
# update() - Add multiple elements
languages.update(["C++", "Rust"])
print(f"After update(['C++', 'Rust']): {languages}")
# remove() - Remove element (raises error if not found)
languages.remove("Java")
print(f"After remove('Java'): {languages}")
# discard() - Remove element (no error if not found)
languages.discard("Go") # No error even though "Go" isn't in set
print(f"After discard('Go'): {languages}")
# pop() - Remove and return an arbitrary element
popped = languages.pop()
print(f"Popped: {popped}, Remaining: {languages}")
# clear() - Remove all elements
temp_set = {1, 2, 3}
temp_set.clear()
print(f"After clear(): {temp_set}")
# -----------------------------------------------------------------------------
# 4. Set Operations - Mathematical Operations
# -----------------------------------------------------------------------------
print("\n--- Set Operations ---")
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}
print(f"Set A: {a}")
print(f"Set B: {b}")
# Union - Elements in A or B or both
union = a | b # or a.union(b)
print(f"\nUnion (A | B): {union}")
# Intersection - Elements in both A and B
intersection = a & b # or a.intersection(b)
print(f"Intersection (A & B): {intersection}")
# Difference - Elements in A but not in B
difference = a - b # or a.difference(b)
print(f"Difference (A - B): {difference}")
difference_ba = b - a
print(f"Difference (B - A): {difference_ba}")
# Symmetric Difference - Elements in A or B but not both
sym_diff = a ^ b # or a.symmetric_difference(b)
print(f"Symmetric Difference (A ^ B): {sym_diff}")
# -----------------------------------------------------------------------------
# 5. Set Comparison Operations
# -----------------------------------------------------------------------------
print("\n--- Set Comparisons ---")
numbers = {1, 2, 3, 4, 5}
subset = {2, 3}
same = {1, 2, 3, 4, 5}
different = {6, 7, 8}
print(f"numbers: {numbers}")
print(f"subset: {subset}")
# issubset() - Is subset contained in numbers?
print(f"\nsubset.issubset(numbers): {subset.issubset(numbers)}")
print(f"subset <= numbers: {subset <= numbers}")
# issuperset() - Does numbers contain subset?
print(f"numbers.issuperset(subset): {numbers.issuperset(subset)}")
print(f"numbers >= subset: {numbers >= subset}")
# isdisjoint() - Do they share any elements?
print(f"\nnumbers.isdisjoint(different): {numbers.isdisjoint(different)}")
print(f"numbers.isdisjoint(subset): {numbers.isdisjoint(subset)}")
# Equality
print(f"\nnumbers == same: {numbers == same}")
print(f"numbers == subset: {numbers == subset}")
# -----------------------------------------------------------------------------
# 6. Update Operations (Modify in Place)
# -----------------------------------------------------------------------------
print("\n--- Update Operations ---")
# These modify the set in place instead of creating new ones
x = {1, 2, 3}
y = {3, 4, 5}
# update() - Union in place (|=)
x_copy = x.copy()
x_copy.update(y) # or x_copy |= y
print(f"After update (union): {x_copy}")
# intersection_update() - Intersection in place (&=)
x_copy = x.copy()
x_copy.intersection_update(y) # or x_copy &= y
print(f"After intersection_update: {x_copy}")
# difference_update() - Difference in place (-=)
x_copy = x.copy()
x_copy.difference_update(y) # or x_copy -= y
print(f"After difference_update: {x_copy}")
# symmetric_difference_update() - Symmetric difference in place (^=)
x_copy = x.copy()
x_copy.symmetric_difference_update(y) # or x_copy ^= y
print(f"After symmetric_difference_update: {x_copy}")
# -----------------------------------------------------------------------------
# 7. Frozen Sets (Immutable Sets)
# -----------------------------------------------------------------------------
print("\n--- Frozen Sets ---")
# Frozen sets are immutable versions of sets
frozen = frozenset([1, 2, 3, 4, 5])
print(f"Frozen set: {frozen}")
# Can perform operations but not modify
print(f"3 in frozen: {3 in frozen}")
print(f"len(frozen): {len(frozen)}")
# These would raise errors:
# frozen.add(6)
# frozen.remove(1)
# Frozen sets can be used as dictionary keys or set elements
nested = {frozenset([1, 2]), frozenset([3, 4])}
print(f"Set of frozensets: {nested}")
# -----------------------------------------------------------------------------
# 8. Set Comprehensions
# -----------------------------------------------------------------------------
print("\n--- Set Comprehensions ---")
# Basic set comprehension
squares = {x**2 for x in range(1, 6)}
print(f"Squares: {squares}")
# With condition
even_squares = {x**2 for x in range(1, 11) if x % 2 == 0}
print(f"Even squares: {even_squares}")
# From string - unique vowels
text = "Hello, World!"
vowels = {char.lower() for char in text if char.lower() in "aeiou"}
print(f"Unique vowels in '{text}': {vowels}")
# -----------------------------------------------------------------------------
# 9. Practical Use Cases
# -----------------------------------------------------------------------------
print("\n--- Practical Use Cases ---")
# 1. Remove duplicates from a list
my_list = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique_list = list(set(my_list))
print(f"Remove duplicates: {my_list}{unique_list}")
# 2. Find common elements
list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
common = set(list1) & set(list2)
print(f"Common elements: {common}")
# 3. Find unique elements across lists
all_elements = set(list1) | set(list2)
print(f"All unique elements: {all_elements}")
# 4. Check if all required items exist
required_skills = {"Python", "SQL", "Git"}
candidate_skills = {"Python", "SQL", "Git", "Docker", "AWS"}
has_all_required = required_skills.issubset(candidate_skills)
print(f"\nCandidate has all required skills: {has_all_required}")
# 5. Find missing items
available_items = {"apple", "banana", "orange"}
shopping_list = {"apple", "milk", "bread", "banana"}
need_to_buy = shopping_list - available_items
print(f"Need to buy: {need_to_buy}")
# 6. Count unique words
text = "the quick brown fox jumps over the lazy dog"
unique_words = set(text.split())
print(f"\nUnique words in text: {len(unique_words)}")
print(f"Words: {unique_words}")
# -----------------------------------------------------------------------------
# 10. Performance: Sets vs Lists for Membership Testing
# -----------------------------------------------------------------------------
print("\n--- Performance Note ---")
# Sets use hash tables - O(1) lookup
# Lists use linear search - O(n) lookup
big_list = list(range(10000))
big_set = set(range(10000))
# For membership testing:
# 9999 in big_list # Slow - checks up to 10000 elements
# 9999 in big_set # Fast - direct hash lookup
print("For membership testing, sets are MUCH faster than lists!")
print("Use sets when you frequently check if items exist in a collection.")

View File

@ -0,0 +1,351 @@
"""
================================================================================
File: 04_dictionaries.py
Topic: Python Dictionaries - Key-Value Pair Collections
================================================================================
This file demonstrates Python dictionaries, which store data as key-value pairs.
Dictionaries are extremely versatile and provide fast access to values using
their associated keys.
Key Concepts:
- Creating and accessing dictionaries
- Adding, modifying, and removing key-value pairs
- Dictionary methods
- Iterating over dictionaries
- Nested dictionaries
- Dictionary comprehensions
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Creating Dictionaries
# -----------------------------------------------------------------------------
# Dictionaries use curly braces with key: value syntax
print("--- Creating Dictionaries ---")
# Empty dictionary
empty_dict = {}
also_empty = dict()
print(f"Empty dict: {empty_dict}")
# Dictionary with elements
person = {
"name": "Baraa",
"age": 25,
"city": "Gaza",
"is_developer": True
}
print(f"Person: {person}")
# Using dict() constructor
from_pairs = dict([("a", 1), ("b", 2), ("c", 3)])
from_kwargs = dict(x=10, y=20, z=30)
print(f"From pairs: {from_pairs}")
print(f"From kwargs: {from_kwargs}")
# Keys can be any immutable type
mixed_keys = {
"string_key": "value1",
42: "value2",
(1, 2): "value3", # Tuple as key
True: "value4"
}
print(f"Mixed keys: {mixed_keys}")
# -----------------------------------------------------------------------------
# 2. Accessing Values
# -----------------------------------------------------------------------------
print("\n--- Accessing Values ---")
student = {
"name": "Ali",
"age": 20,
"grades": [85, 90, 78],
"active": True
}
# Using square brackets
print(f"Name: {student['name']}")
print(f"Grades: {student['grades']}")
# Using get() - safer, returns None if key doesn't exist
print(f"Age: {student.get('age')}")
print(f"Email: {student.get('email')}") # Returns None
print(f"Email with default: {student.get('email', 'Not provided')}")
# Accessing nested values
print(f"First grade: {student['grades'][0]}")
# -----------------------------------------------------------------------------
# 3. Modifying Dictionaries
# -----------------------------------------------------------------------------
print("\n--- Modifying Dictionaries ---")
config = {"theme": "dark", "font_size": 14}
print(f"Original: {config}")
# Update existing key
config["font_size"] = 16
print(f"After update: {config}")
# Add new key
config["language"] = "English"
print(f"After adding key: {config}")
# Update multiple keys at once
config.update({"theme": "light", "auto_save": True})
print(f"After update(): {config}")
# Using setdefault() - only adds if key doesn't exist
config.setdefault("font_size", 20) # Won't change (key exists)
config.setdefault("new_key", "default_value") # Will add
print(f"After setdefault(): {config}")
# -----------------------------------------------------------------------------
# 4. Removing Items
# -----------------------------------------------------------------------------
print("\n--- Removing Items ---")
data = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
print(f"Original: {data}")
# pop() - Remove and return value
removed = data.pop("c")
print(f"Popped 'c': {removed}, Dict: {data}")
# pop() with default - no error if key missing
removed = data.pop("z", "Not found")
print(f"Popped 'z': {removed}")
# popitem() - Remove and return last inserted pair
last_item = data.popitem()
print(f"Popitem: {last_item}, Dict: {data}")
# del - Delete specific key
del data["b"]
print(f"After del 'b': {data}")
# clear() - Remove all items
temp = {"x": 1, "y": 2}
temp.clear()
print(f"After clear(): {temp}")
# -----------------------------------------------------------------------------
# 5. Dictionary Methods
# -----------------------------------------------------------------------------
print("\n--- Dictionary Methods ---")
info = {"name": "Sara", "age": 28, "job": "Engineer"}
# keys() - Get all keys
print(f"Keys: {list(info.keys())}")
# values() - Get all values
print(f"Values: {list(info.values())}")
# items() - Get all key-value pairs
print(f"Items: {list(info.items())}")
# Check if key exists
print(f"\n'name' in info: {'name' in info}")
print(f"'email' in info: {'email' in info}")
# Copy
original = {"a": 1, "b": 2}
copied = original.copy()
copied["c"] = 3
print(f"\nOriginal: {original}")
print(f"Copy: {copied}")
# fromkeys() - Create dict with same value for all keys
keys = ["x", "y", "z"]
default_dict = dict.fromkeys(keys, 0)
print(f"From keys: {default_dict}")
# -----------------------------------------------------------------------------
# 6. Iterating Over Dictionaries
# -----------------------------------------------------------------------------
print("\n--- Iterating Over Dictionaries ---")
scores = {"Alice": 95, "Bob": 87, "Charlie": 92}
# Iterate over keys (default)
print("Keys:")
for key in scores:
print(f" {key}")
# Iterate over values
print("\nValues:")
for value in scores.values():
print(f" {value}")
# Iterate over key-value pairs
print("\nKey-Value pairs:")
for name, score in scores.items():
print(f" {name}: {score}")
# With enumerate (if you need index)
print("\nWith index:")
for idx, (name, score) in enumerate(scores.items(), 1):
print(f" {idx}. {name} scored {score}")
# -----------------------------------------------------------------------------
# 7. Nested Dictionaries
# -----------------------------------------------------------------------------
print("\n--- Nested Dictionaries ---")
# Dictionary containing dictionaries
company = {
"engineering": {
"lead": "Alice",
"members": ["Bob", "Charlie"],
"budget": 50000
},
"marketing": {
"lead": "David",
"members": ["Eve", "Frank"],
"budget": 30000
},
"hr": {
"lead": "Grace",
"members": ["Henry"],
"budget": 20000
}
}
print("Company structure:")
for dept, details in company.items():
print(f"\n {dept.title()} Department:")
print(f" Lead: {details['lead']}")
print(f" Members: {details['members']}")
print(f" Budget: ${details['budget']:,}")
# Access nested values
print(f"\nEngineering lead: {company['engineering']['lead']}")
print(f"Marketing members: {company['marketing']['members']}")
# Modify nested value
company['hr']['budget'] = 25000
print(f"Updated HR budget: {company['hr']['budget']}")
# -----------------------------------------------------------------------------
# 8. Dictionary Comprehensions
# -----------------------------------------------------------------------------
print("\n--- Dictionary Comprehensions ---")
# Basic dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
print(f"Squares: {squares}")
# With condition
even_squares = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(f"Even squares: {even_squares}")
# Transform existing dictionary
original = {"a": 1, "b": 2, "c": 3}
doubled = {k: v * 2 for k, v in original.items()}
print(f"Doubled values: {doubled}")
# Swap keys and values
flipped = {v: k for k, v in original.items()}
print(f"Flipped: {flipped}")
# Filter dictionary
scores = {"Alice": 95, "Bob": 67, "Charlie": 82, "David": 55}
passing = {name: score for name, score in scores.items() if score >= 70}
print(f"Passing students: {passing}")
# From two lists
keys = ["name", "age", "city"]
values = ["John", 30, "NYC"]
combined = {k: v for k, v in zip(keys, values)}
print(f"Combined: {combined}")
# -----------------------------------------------------------------------------
# 9. Merging Dictionaries
# -----------------------------------------------------------------------------
print("\n--- Merging Dictionaries ---")
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
dict3 = {"b": 99, "e": 5} # Note: 'b' also exists in dict1
# Method 1: update() - modifies in place
merged = dict1.copy()
merged.update(dict2)
print(f"Using update(): {merged}")
# Method 2: ** unpacking (Python 3.5+)
merged = {**dict1, **dict2}
print(f"Using ** unpacking: {merged}")
# Method 3: | operator (Python 3.9+)
merged = dict1 | dict2
print(f"Using | operator: {merged}")
# Later values overwrite earlier ones
merged = {**dict1, **dict3} # 'b' will be 99
print(f"With overlap: {merged}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# 1. Word frequency counter
text = "the quick brown fox jumps over the lazy dog the fox"
words = text.split()
frequency = {}
for word in words:
frequency[word] = frequency.get(word, 0) + 1
print(f"Word frequency: {frequency}")
# 2. Grouping items
students = [
{"name": "Alice", "grade": "A"},
{"name": "Bob", "grade": "B"},
{"name": "Charlie", "grade": "A"},
{"name": "David", "grade": "B"},
{"name": "Eve", "grade": "A"}
]
by_grade = {}
for student in students:
grade = student["grade"]
if grade not in by_grade:
by_grade[grade] = []
by_grade[grade].append(student["name"])
print(f"\nStudents by grade: {by_grade}")
# 3. Using dict as simple cache/memo
cache = {}
def fibonacci(n):
if n in cache:
return cache[n]
if n <= 1:
return n
result = fibonacci(n-1) + fibonacci(n-2)
cache[n] = result
return result
print(f"\nFibonacci(10): {fibonacci(10)}")
print(f"Cache: {cache}")
# 4. Configuration settings
default_config = {"theme": "dark", "font": "Arial", "size": 12}
user_config = {"theme": "light", "size": 14}
final_config = {**default_config, **user_config}
print(f"\nFinal config: {final_config}")

View File

@ -0,0 +1,337 @@
"""
================================================================================
File: 01_function_basics.py
Topic: Python Functions - Basic Concepts
================================================================================
This file demonstrates the fundamentals of functions in Python. Functions
are reusable blocks of code that perform specific tasks, making your code
more organized, readable, and maintainable.
Key Concepts:
- Defining functions with def
- Calling functions
- Function documentation (docstrings)
- Variable scope (local vs global)
- Basic parameters
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Defining and Calling Functions
# -----------------------------------------------------------------------------
# Use 'def' keyword to define a function
print("--- Defining and Calling Functions ---")
# Simple function with no parameters
def greet():
"""Print a greeting message."""
print("Hello, World!")
# Call the function
greet()
# Function with a parameter
def greet_person(name):
"""Greet a specific person."""
print(f"Hello, {name}!")
greet_person("Baraa")
greet_person("Sara")
# -----------------------------------------------------------------------------
# 2. Function with Multiple Parameters
# -----------------------------------------------------------------------------
print("\n--- Multiple Parameters ---")
def introduce(name, age, city):
"""Introduce a person with their details."""
print(f"My name is {name}, I am {age} years old, and I live in {city}.")
introduce("Ali", 25, "Cairo")
# -----------------------------------------------------------------------------
# 3. Return Values
# -----------------------------------------------------------------------------
# Functions can return values using 'return'
print("\n--- Return Values ---")
def add(a, b):
"""Add two numbers and return the result."""
return a + b
result = add(5, 3)
print(f"5 + 3 = {result}")
# Using returned value in expressions
total = add(10, 20) + add(5, 5)
print(f"(10+20) + (5+5) = {total}")
# Function without explicit return returns None
def print_message(msg):
print(msg)
result = print_message("Hello")
print(f"Function without return: {result}") # None
# Early return
def get_grade(score):
"""Return letter grade based on score."""
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
if score >= 60:
return "D"
return "F"
print(f"\nScore 85 → Grade {get_grade(85)}")
# -----------------------------------------------------------------------------
# 4. Docstrings - Function Documentation
# -----------------------------------------------------------------------------
# Use triple quotes to document what a function does
print("\n--- Docstrings ---")
def calculate_area(length, width):
"""
Calculate the area of a rectangle.
Args:
length: The length of the rectangle (positive number)
width: The width of the rectangle (positive number)
Returns:
The area of the rectangle (length * width)
Example:
>>> calculate_area(5, 3)
15
"""
return length * width
# Access docstring
print(f"Function docstring:\n{calculate_area.__doc__}")
# Using help()
# help(calculate_area) # Uncomment to see full help
# -----------------------------------------------------------------------------
# 5. Variable Scope - Local vs Global
# -----------------------------------------------------------------------------
# Variables inside functions are local by default
print("\n--- Variable Scope ---")
global_var = "I am global"
def demonstrate_scope():
"""Demonstrate variable scope."""
local_var = "I am local"
print(f" Inside function - global_var: {global_var}")
print(f" Inside function - local_var: {local_var}")
demonstrate_scope()
print(f"Outside function - global_var: {global_var}")
# print(local_var) # This would cause an error!
# Modifying global variables inside functions
counter = 0
def increment_counter():
"""Increment the global counter."""
global counter # Declare we want to modify global variable
counter += 1
print(f" Counter inside function: {counter}")
print(f"\nCounter before: {counter}")
increment_counter()
increment_counter()
print(f"Counter after: {counter}")
# Local variable shadows global
value = 100
def shadow_example():
"""Local variable shadows global."""
value = 200 # This is a new local variable, not the global one
print(f" Inside function: {value}")
print(f"\nGlobal value: {value}")
shadow_example()
print(f"Global value unchanged: {value}")
# -----------------------------------------------------------------------------
# 6. Multiple Return Values
# -----------------------------------------------------------------------------
# Functions can return multiple values as a tuple
print("\n--- Multiple Return Values ---")
def get_min_max(numbers):
"""Return both minimum and maximum of a list."""
return min(numbers), max(numbers)
data = [5, 2, 8, 1, 9, 3]
minimum, maximum = get_min_max(data)
print(f"List: {data}")
print(f"Min: {minimum}, Max: {maximum}")
# Return multiple named values using dictionary
def analyze_text(text):
"""Analyze text and return statistics."""
return {
"length": len(text),
"words": len(text.split()),
"uppercase": sum(1 for c in text if c.isupper())
}
stats = analyze_text("Hello World! How Are You?")
print(f"\nText stats: {stats}")
# -----------------------------------------------------------------------------
# 7. Pass Statement - Placeholder Functions
# -----------------------------------------------------------------------------
# Use pass to create empty function bodies
print("\n--- Placeholder Functions ---")
def future_feature():
"""This will be implemented later."""
pass # Placeholder - does nothing
def another_placeholder():
"""Placeholder with ellipsis (also valid)."""
... # Alternative to pass
future_feature() # Can be called, just does nothing
print("Placeholder functions work!")
# -----------------------------------------------------------------------------
# 8. Nested Functions
# -----------------------------------------------------------------------------
# Functions can be defined inside other functions
print("\n--- Nested Functions ---")
def outer_function(message):
"""Outer function that contains an inner function."""
def inner_function():
"""Inner function that uses outer's variable."""
print(f" Inner says: {message}")
print("Outer function called")
inner_function()
outer_function("Hello from outer!")
# Inner function not accessible outside
# inner_function() # This would cause an error!
# Practical example: Helper function
def calculate_statistics(numbers):
"""Calculate various statistics using helper functions."""
def mean(nums):
return sum(nums) / len(nums)
def variance(nums):
avg = mean(nums)
return sum((x - avg) ** 2 for x in nums) / len(nums)
return {
"mean": mean(numbers),
"variance": variance(numbers),
"std_dev": variance(numbers) ** 0.5
}
data = [2, 4, 4, 4, 5, 5, 7, 9]
stats = calculate_statistics(data)
print(f"\nStatistics for {data}:")
for key, value in stats.items():
print(f" {key}: {value:.2f}")
# -----------------------------------------------------------------------------
# 9. Functions as Objects
# -----------------------------------------------------------------------------
# In Python, functions are first-class objects
print("\n--- Functions as Objects ---")
def say_hello(name):
"""Just say hello."""
return f"Hello, {name}!"
# Assign function to variable
greeting_func = say_hello
print(greeting_func("World"))
# Store functions in a list
def add_one(x): return x + 1
def double(x): return x * 2
def square(x): return x * x
operations = [add_one, double, square]
value = 5
print(f"\nApplying operations to {value}:")
for func in operations:
print(f" {func.__name__}({value}) = {func(value)}")
# Pass function as argument
def apply_operation(func, value):
"""Apply a function to a value."""
return func(value)
print(f"\napply_operation(double, 10) = {apply_operation(double, 10)}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Temperature converter
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
print(f"20°C = {celsius_to_fahrenheit(20):.1f}°F")
print(f"68°F = {fahrenheit_to_celsius(68):.1f}°C")
# Password validator
def is_valid_password(password):
"""
Check if password meets requirements:
- At least 8 characters
- Contains uppercase letter
- Contains lowercase letter
- Contains digit
"""
if len(password) < 8:
return False, "Too short"
if not any(c.isupper() for c in password):
return False, "No uppercase letter"
if not any(c.islower() for c in password):
return False, "No lowercase letter"
if not any(c.isdigit() for c in password):
return False, "No digit"
return True, "Valid password"
test_passwords = ["short", "alllowercase", "ALLUPPERCASE", "ValidPass123"]
print("\nPassword validation:")
for pwd in test_passwords:
is_valid, message = is_valid_password(pwd)
print(f" '{pwd}': {message}")

View File

@ -0,0 +1,308 @@
"""
================================================================================
File: 02_arguments.py
Topic: Function Arguments in Python
================================================================================
This file demonstrates the different ways to pass arguments to functions
in Python. Understanding these patterns is essential for writing flexible
and reusable functions.
Key Concepts:
- Positional arguments
- Keyword arguments
- Default parameter values
- *args (variable positional arguments)
- **kwargs (variable keyword arguments)
- Argument unpacking
- Keyword-only and positional-only arguments
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Positional Arguments
# -----------------------------------------------------------------------------
# Arguments passed in order; position matters
print("--- Positional Arguments ---")
def greet(first_name, last_name):
"""Greet using positional arguments."""
print(f"Hello, {first_name} {last_name}!")
greet("John", "Doe") # Correct order
greet("Jane", "Smith") # Correct order
# Order matters!
greet("Doe", "John") # Wrong order gives wrong result
# -----------------------------------------------------------------------------
# 2. Keyword Arguments
# -----------------------------------------------------------------------------
# Arguments passed by name; order doesn't matter
print("\n--- Keyword Arguments ---")
def create_profile(name, age, city):
"""Create a profile using keyword arguments."""
print(f"Name: {name}, Age: {age}, City: {city}")
# Using keyword arguments (order doesn't matter)
create_profile(name="Alice", age=25, city="NYC")
create_profile(city="Tokyo", name="Bob", age=30)
# Mix positional and keyword (positional must come first!)
create_profile("Charlie", city="London", age=28)
# -----------------------------------------------------------------------------
# 3. Default Parameter Values
# -----------------------------------------------------------------------------
# Parameters can have default values
print("\n--- Default Parameter Values ---")
def greet_with_title(name, title="Mr."):
"""Greet with an optional title."""
print(f"Hello, {title} {name}!")
greet_with_title("Smith") # Uses default title
greet_with_title("Johnson", "Dr.") # Overrides default
# Multiple defaults
def create_user(username, email, role="user", active=True):
"""Create a user with defaults."""
print(f"User: {username}, Email: {email}, Role: {role}, Active: {active}")
create_user("john", "john@example.com")
create_user("admin", "admin@example.com", role="admin")
create_user("test", "test@example.com", active=False)
# CAUTION: Mutable default arguments
# DON'T DO THIS:
def bad_function(items=[]): # Bad! List is shared across calls
items.append(1)
return items
# DO THIS INSTEAD:
def good_function(items=None):
"""Safe way to handle mutable defaults."""
if items is None:
items = []
items.append(1)
return items
print(f"\nBad function calls: {bad_function()}, {bad_function()}, {bad_function()}")
print(f"Good function calls: {good_function()}, {good_function()}, {good_function()}")
# -----------------------------------------------------------------------------
# 4. *args - Variable Positional Arguments
# -----------------------------------------------------------------------------
# Accept any number of positional arguments
print("\n--- *args (Variable Positional Arguments) ---")
def sum_all(*args):
"""Sum all provided numbers."""
print(f" Received args: {args} (type: {type(args).__name__})")
return sum(args)
print(f"sum_all(1, 2): {sum_all(1, 2)}")
print(f"sum_all(1, 2, 3, 4, 5): {sum_all(1, 2, 3, 4, 5)}")
print(f"sum_all(): {sum_all()}")
# Combining regular parameters with *args
def greet_people(greeting, *names):
"""Greet multiple people with the same greeting."""
for name in names:
print(f" {greeting}, {name}!")
print("\nGreeting people:")
greet_people("Hello", "Alice", "Bob", "Charlie")
# -----------------------------------------------------------------------------
# 5. **kwargs - Variable Keyword Arguments
# -----------------------------------------------------------------------------
# Accept any number of keyword arguments
print("\n--- **kwargs (Variable Keyword Arguments) ---")
def print_info(**kwargs):
"""Print all keyword arguments."""
print(f" Received kwargs: {kwargs} (type: {type(kwargs).__name__})")
for key, value in kwargs.items():
print(f" {key}: {value}")
print_info(name="Baraa", age=25, city="Gaza")
print_info(language="Python", level="Expert")
# Combining all parameter types
def complete_example(required, *args, default="value", **kwargs):
"""Demonstrate all parameter types."""
print(f" Required: {required}")
print(f" *args: {args}")
print(f" Default: {default}")
print(f" **kwargs: {kwargs}")
print("\nComplete example:")
complete_example("must_have", 1, 2, 3, default="custom", extra="data", more="stuff")
# -----------------------------------------------------------------------------
# 6. Argument Unpacking
# -----------------------------------------------------------------------------
# Use * and ** to unpack sequences and dictionaries into arguments
print("\n--- Argument Unpacking ---")
def introduce(name, age, city):
"""Introduce a person."""
print(f"I'm {name}, {age} years old, from {city}")
# Unpack list/tuple with *
person_list = ["Alice", 30, "Paris"]
introduce(*person_list) # Same as: introduce("Alice", 30, "Paris")
# Unpack dictionary with **
person_dict = {"name": "Bob", "age": 25, "city": "Berlin"}
introduce(**person_dict) # Same as: introduce(name="Bob", age=25, city="Berlin")
# Combine unpacking
def multiply(a, b, c):
return a * b * c
nums = [2, 3]
print(f"\nMultiply with unpacking: {multiply(*nums, 4)}")
# -----------------------------------------------------------------------------
# 7. Keyword-Only Arguments (Python 3+)
# -----------------------------------------------------------------------------
# Arguments after * must be passed as keywords
print("\n--- Keyword-Only Arguments ---")
def format_name(first, last, *, upper=False, reverse=False):
"""
Format a name. 'upper' and 'reverse' are keyword-only.
"""
name = f"{first} {last}"
if reverse:
name = f"{last}, {first}"
if upper:
name = name.upper()
return name
# These work:
print(format_name("John", "Doe"))
print(format_name("John", "Doe", upper=True))
print(format_name("John", "Doe", reverse=True, upper=True))
# This would fail:
# format_name("John", "Doe", True) # Error! upper must be keyword
# Using bare * for keyword-only
def connect(*, host, port, timeout=30):
"""All parameters are keyword-only."""
print(f" Connecting to {host}:{port} (timeout: {timeout}s)")
connect(host="localhost", port=8080)
connect(host="db.example.com", port=5432, timeout=60)
# -----------------------------------------------------------------------------
# 8. Positional-Only Arguments (Python 3.8+)
# -----------------------------------------------------------------------------
# Arguments before / must be passed positionally
print("\n--- Positional-Only Arguments ---")
def divide(x, y, /):
"""x and y must be passed positionally."""
return x / y
print(f"divide(10, 2): {divide(10, 2)}")
# divide(x=10, y=2) # Error! x and y are positional-only
# Combining positional-only, regular, and keyword-only
def complex_function(pos_only1, pos_only2, /, regular1, regular2, *, kw_only1, kw_only2="default"):
"""
pos_only1, pos_only2: positional-only (before /)
regular1, regular2: can be either
kw_only1, kw_only2: keyword-only (after *)
"""
print(f" pos_only: {pos_only1}, {pos_only2}")
print(f" regular: {regular1}, {regular2}")
print(f" kw_only: {kw_only1}, {kw_only2}")
print("\nComplex function:")
complex_function(1, 2, 3, regular2=4, kw_only1="required")
# -----------------------------------------------------------------------------
# 9. Type Hints for Arguments (Preview)
# -----------------------------------------------------------------------------
# Adding type information (doesn't enforce, just hints)
print("\n--- Type Hints ---")
def calculate_total(price: float, quantity: int, tax_rate: float = 0.1) -> float:
"""
Calculate total price with tax.
Args:
price: Unit price (float)
quantity: Number of items (int)
tax_rate: Tax rate as decimal (default 0.1 = 10%)
Returns:
Total price including tax
"""
subtotal = price * quantity
return subtotal * (1 + tax_rate)
total = calculate_total(19.99, 3)
print(f"Total: ${total:.2f}")
# Type hints with complex types
from typing import List, Optional, Dict
def process_scores(scores: List[int], name: Optional[str] = None) -> Dict[str, float]:
"""Process a list of scores and return statistics."""
return {
"name": name or "Unknown",
"average": sum(scores) / len(scores),
"highest": max(scores),
"lowest": min(scores)
}
result = process_scores([85, 90, 78, 92], "Alice")
print(f"Score stats: {result}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Flexible logging function
def log(message, *tags, level="INFO", **metadata):
"""Log a message with optional tags and metadata."""
tag_str = " ".join(f"[{tag}]" for tag in tags)
meta_str = " | ".join(f"{k}={v}" for k, v in metadata.items())
output = f"[{level}] {tag_str} {message}"
if meta_str:
output += f" ({meta_str})"
print(output)
log("Server started", "server", "startup")
log("User logged in", "auth", level="DEBUG", user_id=123, ip="192.168.1.1")
# Builder pattern with kwargs
def create_html_element(tag, content="", **attributes):
"""Create an HTML element string."""
attrs = " ".join(f'{k}="{v}"' for k, v in attributes.items())
if attrs:
attrs = " " + attrs
return f"<{tag}{attrs}>{content}</{tag}>"
print("\n" + create_html_element("p", "Hello World"))
print(create_html_element("a", "Click me", href="https://example.com", target="_blank"))
print(create_html_element("input", type="text", placeholder="Enter name", id="name_input"))

View File

@ -0,0 +1,399 @@
"""
================================================================================
File: 03_return_values.py
Topic: Function Return Values in Python
================================================================================
This file demonstrates various ways functions can return values in Python,
including returning multiple values, early returns, and more advanced patterns.
Key Concepts:
- Returning single and multiple values
- Returning None (explicit and implicit)
- Early returns for cleaner code
- Returning functions (closures)
- Return type annotations
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic Return Values
# -----------------------------------------------------------------------------
# Use 'return' to send a value back to the caller
print("--- Basic Return Values ---")
def square(n):
"""Return the square of a number."""
return n * n
def add(a, b):
"""Return the sum of two numbers."""
return a + b
result = square(5)
print(f"square(5) = {result}")
# Using return value directly
print(f"add(3, 4) = {add(3, 4)}")
# Chaining function calls
print(f"square(add(2, 3)) = {square(add(2, 3))}")
# -----------------------------------------------------------------------------
# 2. Returning None
# -----------------------------------------------------------------------------
# Functions without explicit return (or with 'return' alone) return None
print("\n--- Returning None ---")
# Implicit None return
def greet(name):
"""Print a greeting - no explicit return."""
print(f" Hello, {name}!")
result = greet("Alice")
print(f"Return value: {result}")
# Explicit None return
def try_parse_int(value):
"""Try to parse string as int, return None on failure."""
try:
return int(value)
except ValueError:
return None # Explicit None
print(f"\ntry_parse_int('42'): {try_parse_int('42')}")
print(f"try_parse_int('abc'): {try_parse_int('abc')}")
# Using None as sentinel value
def find_item(items, target):
"""Find item in list, return index or None."""
for i, item in enumerate(items):
if item == target:
return i
return None # Not found
fruits = ["apple", "banana", "cherry"]
index = find_item(fruits, "banana")
if index is not None:
print(f"\nFound 'banana' at index {index}")
# -----------------------------------------------------------------------------
# 3. Returning Multiple Values
# -----------------------------------------------------------------------------
# Python functions can return multiple values as tuples
print("\n--- Returning Multiple Values ---")
# Return as tuple (implicit)
def get_name_parts(full_name):
"""Split full name into first and last."""
parts = full_name.split()
return parts[0], parts[-1] # Returns tuple
first, last = get_name_parts("John William Doe")
print(f"First: {first}, Last: {last}")
# Return as tuple (explicit)
def min_max(numbers):
"""Return minimum and maximum as explicit tuple."""
return (min(numbers), max(numbers))
result = min_max([3, 1, 4, 1, 5, 9])
print(f"min_max result: {result}")
print(f"Type: {type(result)}")
# Return as dictionary (named values)
def analyze_string(text):
"""Analyze string and return statistics as dict."""
return {
"length": len(text),
"words": len(text.split()),
"chars_no_space": len(text.replace(" ", "")),
"upper_count": sum(1 for c in text if c.isupper()),
"lower_count": sum(1 for c in text if c.islower())
}
stats = analyze_string("Hello World")
print(f"\nString analysis: {stats}")
# Return as named tuple (best of both worlds)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y", "z"])
def create_point(x, y, z):
"""Create a 3D point."""
return Point(x, y, z)
p = create_point(10, 20, 30)
print(f"\nNamed tuple: {p}")
print(f"Access by name: x={p.x}, y={p.y}, z={p.z}")
# -----------------------------------------------------------------------------
# 4. Early Returns (Guard Clauses)
# -----------------------------------------------------------------------------
# Return early to handle edge cases and improve readability
print("\n--- Early Returns ---")
# Without early returns (nested, harder to read)
def get_grade_nested(score):
if score >= 0 and score <= 100:
if score >= 90:
return "A"
else:
if score >= 80:
return "B"
else:
if score >= 70:
return "C"
else:
if score >= 60:
return "D"
else:
return "F"
else:
return "Invalid"
# With early returns (flat, easier to read)
def get_grade_early(score):
"""Get grade using early returns (guard clauses)."""
if score < 0 or score > 100:
return "Invalid"
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
if score >= 60:
return "D"
return "F"
print(f"get_grade_early(85) = {get_grade_early(85)}")
print(f"get_grade_early(150) = {get_grade_early(150)}")
# Practical example: Input validation with early returns
def process_user_data(data):
"""Process user data with validation."""
# Guard clauses
if data is None:
return {"error": "No data provided"}
if not isinstance(data, dict):
return {"error": "Data must be a dictionary"}
if "name" not in data:
return {"error": "Name is required"}
if "age" not in data:
return {"error": "Age is required"}
if data["age"] < 0:
return {"error": "Age must be positive"}
# Main logic (only runs if all validations pass)
return {
"success": True,
"message": f"Processed user {data['name']}, age {data['age']}"
}
print("\nData validation examples:")
print(f" None: {process_user_data(None)}")
print(f" Empty dict: {process_user_data({})}")
print(f" Valid: {process_user_data({'name': 'Alice', 'age': 25})}")
# -----------------------------------------------------------------------------
# 5. Returning Functions (Closures)
# -----------------------------------------------------------------------------
# Functions can return other functions
print("\n--- Returning Functions ---")
def make_multiplier(factor):
"""Return a function that multiplies by factor."""
def multiplier(x):
return x * factor
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(f"double(5) = {double(5)}")
print(f"triple(5) = {triple(5)}")
# Practical: Create custom validators
def make_range_validator(min_val, max_val):
"""Create a validator for a specific range."""
def validate(value):
return min_val <= value <= max_val
return validate
age_validator = make_range_validator(0, 120)
percentage_validator = make_range_validator(0, 100)
print(f"\nage_validator(25) = {age_validator(25)}")
print(f"age_validator(150) = {age_validator(150)}")
print(f"percentage_validator(85.5) = {percentage_validator(85.5)}")
# -----------------------------------------------------------------------------
# 6. Return Type Annotations
# -----------------------------------------------------------------------------
# Add type hints for return values
print("\n--- Return Type Annotations ---")
from typing import List, Optional, Tuple, Dict, Union
def get_greeting(name: str) -> str:
"""Return a greeting string."""
return f"Hello, {name}!"
def divide(a: float, b: float) -> Optional[float]:
"""Divide a by b, return None if b is zero."""
if b == 0:
return None
return a / b
def get_user_info(user_id: int) -> Dict[str, Union[str, int]]:
"""Return user info as dictionary."""
return {"id": user_id, "name": "Test User", "age": 25}
def process_numbers(nums: List[int]) -> Tuple[int, int, float]:
"""Return min, max, and average."""
return min(nums), max(nums), sum(nums) / len(nums)
print(f"get_greeting('World'): {get_greeting('World')}")
print(f"divide(10, 3): {divide(10, 3)}")
print(f"divide(10, 0): {divide(10, 0)}")
print(f"get_user_info(1): {get_user_info(1)}")
print(f"process_numbers([1,2,3,4,5]): {process_numbers([1,2,3,4,5])}")
# -----------------------------------------------------------------------------
# 7. Generator Functions (yield vs return)
# -----------------------------------------------------------------------------
# Functions that yield values instead of returning
print("\n--- Generators (yield) ---")
# Regular function - returns all at once
def get_squares_list(n):
"""Return list of squares."""
return [i ** 2 for i in range(n)]
# Generator function - yields one at a time
def get_squares_generator(n):
"""Generate squares one at a time."""
for i in range(n):
yield i ** 2
# Generator is memory-efficient for large sequences
squares_list = get_squares_list(5)
squares_gen = get_squares_generator(5)
print(f"List: {squares_list}")
print(f"Generator: {squares_gen}")
print(f"From generator: {list(squares_gen)}")
# Practical: Generator for large files (conceptual)
def read_large_file_lines(filename):
"""Yield lines one at a time (memory efficient)."""
# In real code: open(filename) and yield line by line
# This is just a demonstration
for i in range(5):
yield f"Line {i + 1} from {filename}"
print("\nGenerator example:")
for line in read_large_file_lines("data.txt"):
print(f" {line}")
# -----------------------------------------------------------------------------
# 8. Conditional Returns
# -----------------------------------------------------------------------------
# Return different types or values based on conditions
print("\n--- Conditional Returns ---")
# Ternary return
def absolute_value(n):
"""Return absolute value using ternary."""
return n if n >= 0 else -n
print(f"absolute_value(-5) = {absolute_value(-5)}")
print(f"absolute_value(5) = {absolute_value(5)}")
# Short-circuit return with 'or'
def get_username(user):
"""Get username or default."""
return user.get("username") or "anonymous"
print(f"get_username({{}}): {get_username({})}")
print(f"get_username({{'username': 'john'}}): {get_username({'username': 'john'})}")
# Return type based on input
def smart_divide(a, b, as_float=True):
"""Return float or int based on parameter."""
if b == 0:
return None
return a / b if as_float else a // b
print(f"\nsmart_divide(10, 3, True): {smart_divide(10, 3, True)}")
print(f"smart_divide(10, 3, False): {smart_divide(10, 3, False)}")
# -----------------------------------------------------------------------------
# 9. Return vs Print
# -----------------------------------------------------------------------------
# Important distinction for beginners
print("\n--- Return vs Print ---")
def print_double(n):
"""Print double (returns None)."""
print(n * 2)
def return_double(n):
"""Return double."""
return n * 2
# print_double can't be used in expressions
result1 = print_double(5) # Prints 10
print(f"print_double(5) returns: {result1}")
# return_double can be used in expressions
result2 = return_double(5) # Returns 10
print(f"return_double(5) returns: {result2}")
print(f"return_double(5) + 1 = {return_double(5) + 1}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# API-style response
def api_response(success, data=None, error=None):
"""Create standardized API response."""
return {
"success": success,
"data": data,
"error": error
}
print("API responses:")
print(f" Success: {api_response(True, {'user': 'john', 'id': 1})}")
print(f" Error: {api_response(False, error='User not found')}")
# Chain-able operations
def chain_operation(value, operations):
"""Apply a chain of operations to a value."""
result = value
for op in operations:
result = op(result)
return result
ops = [lambda x: x + 10, lambda x: x * 2, lambda x: x - 5]
print(f"\nChained operations on 5: {chain_operation(5, ops)}")
# (5 + 10) * 2 - 5 = 25

View File

@ -0,0 +1,340 @@
"""
================================================================================
File: 04_lambda_functions.py
Topic: Lambda Functions in Python
================================================================================
This file demonstrates lambda functions (anonymous functions) in Python.
Lambda functions are small, one-line functions that can be defined inline
without using the 'def' keyword.
Key Concepts:
- Lambda syntax
- When to use lambda functions
- Lambda with built-in functions (map, filter, sorted)
- Lambda vs regular functions
- Common use cases and patterns
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic Lambda Syntax
# -----------------------------------------------------------------------------
# lambda arguments: expression
print("--- Basic Lambda Syntax ---")
# Regular function
def add_regular(a, b):
return a + b
# Equivalent lambda function
add_lambda = lambda a, b: a + b
print(f"Regular function: {add_regular(3, 5)}")
print(f"Lambda function: {add_lambda(3, 5)}")
# More examples
square = lambda x: x ** 2
is_even = lambda x: x % 2 == 0
greet = lambda name: f"Hello, {name}!"
print(f"\nsquare(4) = {square(4)}")
print(f"is_even(7) = {is_even(7)}")
print(f"greet('Alice') = {greet('Alice')}")
# Lambda with no arguments
get_pi = lambda: 3.14159
print(f"get_pi() = {get_pi()}")
# -----------------------------------------------------------------------------
# 2. Lambda with Multiple Arguments
# -----------------------------------------------------------------------------
print("\n--- Multiple Arguments ---")
# Two arguments
multiply = lambda x, y: x * y
print(f"multiply(4, 5) = {multiply(4, 5)}")
# Three arguments
volume = lambda l, w, h: l * w * h
print(f"volume(2, 3, 4) = {volume(2, 3, 4)}")
# With default arguments
power = lambda base, exp=2: base ** exp
print(f"power(3) = {power(3)}") # 3^2 = 9
print(f"power(2, 3) = {power(2, 3)}") # 2^3 = 8
# -----------------------------------------------------------------------------
# 3. Lambda with Conditional Expression
# -----------------------------------------------------------------------------
# Using ternary operator in lambda
print("\n--- Conditional Lambda ---")
# Simple conditional
get_sign = lambda x: "positive" if x > 0 else ("negative" if x < 0 else "zero")
print(f"get_sign(5) = {get_sign(5)}")
print(f"get_sign(-3) = {get_sign(-3)}")
print(f"get_sign(0) = {get_sign(0)}")
# Max of two numbers
max_of_two = lambda a, b: a if a > b else b
print(f"\nmax_of_two(10, 20) = {max_of_two(10, 20)}")
# Absolute value
absolute = lambda x: x if x >= 0 else -x
print(f"absolute(-7) = {absolute(-7)}")
# -----------------------------------------------------------------------------
# 4. Lambda with map()
# -----------------------------------------------------------------------------
# Apply function to each element of iterable
print("\n--- Lambda with map() ---")
numbers = [1, 2, 3, 4, 5]
# Square each number
squares = list(map(lambda x: x ** 2, numbers))
print(f"Original: {numbers}")
print(f"Squared: {squares}")
# Convert to strings
strings = list(map(lambda x: str(x), numbers))
print(f"As strings: {strings}")
# Multiple iterables with map
list1 = [1, 2, 3]
list2 = [10, 20, 30]
sums = list(map(lambda x, y: x + y, list1, list2))
print(f"\n{list1} + {list2} = {sums}")
# Processing strings
names = ["alice", "bob", "charlie"]
capitalized = list(map(lambda name: name.capitalize(), names))
print(f"Capitalized: {capitalized}")
# -----------------------------------------------------------------------------
# 5. Lambda with filter()
# -----------------------------------------------------------------------------
# Filter elements based on condition
print("\n--- Lambda with filter() ---")
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Filter even numbers
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(f"Original: {numbers}")
print(f"Even only: {evens}")
# Filter numbers greater than 5
greater_than_5 = list(filter(lambda x: x > 5, numbers))
print(f"Greater than 5: {greater_than_5}")
# Filter non-empty strings
strings = ["hello", "", "world", "", "python", ""]
non_empty = list(filter(lambda s: s, strings))
print(f"\nNon-empty strings: {non_empty}")
# Complex filtering
people = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 17},
{"name": "Charlie", "age": 30},
{"name": "David", "age": 15}
]
adults = list(filter(lambda p: p["age"] >= 18, people))
print(f"Adults: {[p['name'] for p in adults]}")
# -----------------------------------------------------------------------------
# 6. Lambda with sorted()
# -----------------------------------------------------------------------------
# Custom sorting with key function
print("\n--- Lambda with sorted() ---")
# Sort by absolute value
numbers = [-5, 2, -1, 7, -3, 4]
by_absolute = sorted(numbers, key=lambda x: abs(x))
print(f"Original: {numbers}")
print(f"Sorted by absolute value: {by_absolute}")
# Sort strings by length
words = ["python", "is", "a", "programming", "language"]
by_length = sorted(words, key=lambda w: len(w))
print(f"\nSorted by length: {by_length}")
# Sort objects by attribute
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78}
]
by_grade = sorted(students, key=lambda s: s["grade"], reverse=True)
print(f"\nBy grade (highest first):")
for s in by_grade:
print(f" {s['name']}: {s['grade']}")
# Sort by multiple criteria
items = [("apple", 3), ("banana", 1), ("cherry", 2), ("apple", 1)]
# Sort by name, then by number
sorted_items = sorted(items, key=lambda x: (x[0], x[1]))
print(f"\nSorted by name, then number: {sorted_items}")
# -----------------------------------------------------------------------------
# 7. Lambda with reduce()
# -----------------------------------------------------------------------------
# Reduce iterable to single value
print("\n--- Lambda with reduce() ---")
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# Sum all numbers
total = reduce(lambda acc, x: acc + x, numbers)
print(f"Sum of {numbers} = {total}")
# Product of all numbers
product = reduce(lambda acc, x: acc * x, numbers)
print(f"Product of {numbers} = {product}")
# Find maximum
maximum = reduce(lambda a, b: a if a > b else b, numbers)
print(f"Maximum of {numbers} = {maximum}")
# Concatenate strings
words = ["Hello", " ", "World", "!"]
sentence = reduce(lambda a, b: a + b, words)
print(f"Concatenated: '{sentence}'")
# With initial value
numbers = [1, 2, 3]
sum_with_initial = reduce(lambda acc, x: acc + x, numbers, 100)
print(f"\nSum with initial 100: {sum_with_initial}")
# -----------------------------------------------------------------------------
# 8. Lambda in Data Processing
# -----------------------------------------------------------------------------
print("\n--- Data Processing Example ---")
# Sample data
transactions = [
{"id": 1, "type": "credit", "amount": 100},
{"id": 2, "type": "debit", "amount": 50},
{"id": 3, "type": "credit", "amount": 200},
{"id": 4, "type": "debit", "amount": 75},
{"id": 5, "type": "credit", "amount": 150}
]
# Filter credits only
credits = list(filter(lambda t: t["type"] == "credit", transactions))
print(f"Credit transactions: {len(credits)}")
# Extract amounts from credits
credit_amounts = list(map(lambda t: t["amount"], credits))
print(f"Credit amounts: {credit_amounts}")
# Total credits
total_credits = reduce(lambda acc, t: acc + t["amount"], credits, 0)
print(f"Total credits: ${total_credits}")
# Combined: total debits in one expression
total_debits = reduce(
lambda acc, x: acc + x,
map(
lambda t: t["amount"],
filter(lambda t: t["type"] == "debit", transactions)
),
0
)
print(f"Total debits: ${total_debits}")
# -----------------------------------------------------------------------------
# 9. Lambda vs Regular Functions
# -----------------------------------------------------------------------------
print("\n--- Lambda vs Regular Functions ---")
# Lambda: one expression, implicit return
# Regular: multiple statements, explicit return
# When to use LAMBDA:
# - Simple, one-line operations
# - As arguments to higher-order functions
# - When function won't be reused
# When to use REGULAR FUNCTIONS:
# - Multiple expressions/statements needed
# - Need docstrings
# - Function will be reused or tested
# - Complex logic
# Example: Complex logic needs regular function
def process_value(x):
"""Process value with multiple steps."""
# Step 1: Validate
if x < 0:
return None
# Step 2: Transform
result = x ** 2
# Step 3: Apply ceiling
if result > 100:
result = 100
return result
# This CAN'T be easily done with lambda
# lambda x: (None if x < 0 else min(x ** 2, 100)) # Gets messy
print(f"process_value(-5) = {process_value(-5)}")
print(f"process_value(5) = {process_value(5)}")
print(f"process_value(15) = {process_value(15)}")
# -----------------------------------------------------------------------------
# 10. Common Lambda Patterns
# -----------------------------------------------------------------------------
print("\n--- Common Lambda Patterns ---")
# 1. Default value getter
get_value = lambda d, key, default=None: d.get(key, default)
data = {"name": "John", "age": 30}
print(f"get_value: {get_value(data, 'name')}, {get_value(data, 'email', 'N/A')}")
# 2. Compose functions
compose = lambda f, g: lambda x: f(g(x))
add_one = lambda x: x + 1
double = lambda x: x * 2
add_then_double = compose(double, add_one)
print(f"add_then_double(5) = {add_then_double(5)}") # (5+1)*2 = 12
# 3. Partial application simulation
multiply_by = lambda n: lambda x: x * n
times_three = multiply_by(3)
print(f"times_three(7) = {times_three(7)}")
# 4. Key extractors for sorting
by_key = lambda key: lambda x: x[key]
products = [
{"name": "Apple", "price": 1.50},
{"name": "Banana", "price": 0.75},
{"name": "Cherry", "price": 2.00}
]
sorted_by_price = sorted(products, key=by_key("price"))
print(f"\nSorted by price: {[p['name'] for p in sorted_by_price]}")
# 5. Immediate invocation (IIFE-style)
result = (lambda x, y: x ** y)(2, 10)
print(f"\nImmediately invoked: 2^10 = {result}")

View File

@ -0,0 +1,273 @@
"""
================================================================================
File: 01_imports.py
Topic: Importing Modules and Packages in Python
================================================================================
This file demonstrates how to import and use modules in Python. Modules are
files containing Python code that can be reused across different programs.
Python's extensive standard library and third-party packages make imports
essential for productive development.
Key Concepts:
- import statement
- from ... import ...
- Aliasing with 'as'
- Built-in and standard library modules
- Relative imports
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic Import Statement
# -----------------------------------------------------------------------------
# Import entire module
print("--- Basic Import ---")
import math
# Access functions using module.function
print(f"math.pi = {math.pi}")
print(f"math.sqrt(16) = {math.sqrt(16)}")
print(f"math.ceil(3.2) = {math.ceil(3.2)}")
print(f"math.floor(3.8) = {math.floor(3.8)}")
# -----------------------------------------------------------------------------
# 2. Import Specific Items
# -----------------------------------------------------------------------------
# Import only what you need
print("\n--- From Import ---")
from math import pi, sqrt, pow
# Use directly without module prefix
print(f"pi = {pi}")
print(f"sqrt(25) = {sqrt(25)}")
print(f"pow(2, 8) = {pow(2, 8)}")
# Import multiple items
from datetime import date, time, datetime
today = date.today()
print(f"\nToday's date: {today}")
now = datetime.now()
print(f"Current datetime: {now}")
# -----------------------------------------------------------------------------
# 3. Import with Alias
# -----------------------------------------------------------------------------
# Rename modules or items for convenience
print("\n--- Import with Alias ---")
# Module alias
import random as rnd
print(f"Random number: {rnd.randint(1, 100)}")
print(f"Random choice: {rnd.choice(['apple', 'banana', 'cherry'])}")
# Common conventions
import collections as col
import json as json # Usually kept as is
import os as os # Usually kept as is
# Item alias
from math import factorial as fact
print(f"\nfact(5) = {fact(5)}")
# -----------------------------------------------------------------------------
# 4. Import All (Use Sparingly)
# -----------------------------------------------------------------------------
# Import everything from a module
print("\n--- Import All (Not Recommended) ---")
# from math import *
# This imports everything, but:
# - Can cause naming conflicts
# - Makes it unclear where functions come from
# - Only use in interactive sessions if at all
# Better to be explicit about what you import
# -----------------------------------------------------------------------------
# 5. Useful Standard Library Modules
# -----------------------------------------------------------------------------
print("\n--- Standard Library Examples ---")
# os - Operating system interface
import os
print(f"Current directory: {os.getcwd()}")
print(f"Directory separator: {os.sep}")
# sys - System-specific parameters
import sys
print(f"\nPython version: {sys.version_info.major}.{sys.version_info.minor}")
# collections - Specialized containers
from collections import Counter, defaultdict
word_counts = Counter("mississippi")
print(f"\nCharacter counts: {dict(word_counts)}")
# Default dictionary
dd = defaultdict(list)
dd["fruits"].append("apple")
dd["fruits"].append("banana")
dd["vegetables"].append("carrot")
print(f"defaultdict: {dict(dd)}")
# json - JSON encoding/decoding
import json
data = {"name": "Alice", "age": 30}
json_string = json.dumps(data, indent=2)
print(f"\nJSON string:\n{json_string}")
# re - Regular expressions
import re
text = "Contact: john@email.com or jane@company.org"
emails = re.findall(r'\b[\w.-]+@[\w.-]+\.\w+\b', text)
print(f"Found emails: {emails}")
# itertools - Iteration utilities
from itertools import combinations, permutations
items = ['A', 'B', 'C']
print(f"\nCombinations of 2: {list(combinations(items, 2))}")
print(f"Permutations of 2: {list(permutations(items, 2))}")
# functools - Higher-order functions
from functools import lru_cache
@lru_cache(maxsize=100)
def fibonacci(n):
"""Cached fibonacci for performance."""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(f"\nFibonacci(30): {fibonacci(30)}")
print(f"Cache info: {fibonacci.cache_info()}")
# -----------------------------------------------------------------------------
# 6. Checking Module Attributes
# -----------------------------------------------------------------------------
print("\n--- Module Attributes ---")
import math
# List all attributes/functions in a module
print("Math module functions (first 10):")
for name in dir(math)[:10]:
print(f" {name}")
# Module docstring
print(f"\nMath module doc: {math.__doc__[:50]}...")
# Module file location
print(f"Math module file: {math.__file__}")
# -----------------------------------------------------------------------------
# 7. Conditional Imports
# -----------------------------------------------------------------------------
print("\n--- Conditional Imports ---")
# Try to import, use fallback if not available
try:
import numpy as np
HAS_NUMPY = True
print("NumPy is available")
except ImportError:
HAS_NUMPY = False
print("NumPy is NOT available")
# Another pattern
import sys
if sys.version_info >= (3, 9):
from typing import Annotated # Python 3.9+
else:
print("Annotated not available (Python < 3.9)")
# -----------------------------------------------------------------------------
# 8. Module Search Path
# -----------------------------------------------------------------------------
print("\n--- Module Search Path ---")
import sys
print("Python searches for modules in:")
for i, path in enumerate(sys.path[:5]): # First 5 paths
print(f" {i+1}. {path}")
print(f" ... and {len(sys.path) - 5} more locations")
# Adding custom path (temporarily)
# sys.path.append('/my/custom/modules')
# -----------------------------------------------------------------------------
# 9. Import Patterns in Projects
# -----------------------------------------------------------------------------
print("\n--- Import Best Practices ---")
# Standard order for imports:
# 1. Standard library imports
# 2. Third-party imports
# 3. Local/project imports
# Example of well-organized imports:
"""
# Standard library
import os
import sys
from datetime import datetime
from typing import List, Dict
# Third-party (installed via pip)
import requests
import numpy as np
import pandas as pd
# Local/project modules
from myproject.utils import helper
from myproject.models import User
"""
print("Import order: Standard -> Third-party -> Local")
print("Group imports with blank lines between groups")
# -----------------------------------------------------------------------------
# 10. Practical Example: Building a Utility
# -----------------------------------------------------------------------------
print("\n--- Practical Example ---")
# Using multiple modules together
import os
import json
from datetime import datetime
from pathlib import Path
def get_system_info():
"""Gather system information using various modules."""
return {
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
"platform": sys.platform,
"cwd": os.getcwd(),
"home_dir": str(Path.home()),
"timestamp": datetime.now().isoformat(),
"path_separator": os.sep
}
info = get_system_info()
print("System Info:")
print(json.dumps(info, indent=2))

View File

@ -0,0 +1,443 @@
"""
================================================================================
File: 02_custom_modules.py
Topic: Creating and Using Custom Modules
================================================================================
This file demonstrates how to create your own modules and packages in Python.
Custom modules help organize code, promote reusability, and maintain clean
project structures.
Key Concepts:
- Creating module files
- The __name__ variable
- __init__.py for packages
- Relative imports
- Module documentation
- Publishing modules
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. What is a Module?
# -----------------------------------------------------------------------------
# A module is simply a .py file containing Python code
print("--- What is a Module? ---")
# Any Python file is a module!
# If you have: my_utils.py
# You can: import my_utils
# Example module content (imagine this is saved as 'my_math.py'):
"""
# my_math.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
PI = 3.14159
class Calculator:
def multiply(self, a, b):
return a * b
"""
print("A module is just a Python file that can be imported.")
print("It can contain functions, classes, and variables.")
# -----------------------------------------------------------------------------
# 2. The __name__ Variable
# -----------------------------------------------------------------------------
# __name__ tells you how the module is being used
print("\n--- The __name__ Variable ---")
# When a file is run directly: __name__ == "__main__"
# When a file is imported: __name__ == module_name
print(f"Current __name__: {__name__}")
# Common pattern for executable modules:
"""
# utils.py
def useful_function():
return "I'm useful!"
def main():
print("Running as main program")
print(useful_function())
# This block only runs when file is executed directly
if __name__ == "__main__":
main()
"""
# Demo of the pattern
def demo_function():
"""A function that would be in a module."""
return "Hello from demo!"
def main():
"""Main function that runs when executed directly."""
print("This module is being run directly!")
print(demo_function())
# This is the __name__ check pattern
if __name__ == "__main__":
# This runs only when this file is executed directly
# Not when it's imported
pass # In real code, you'd call main()
print("\nThe 'if __name__ == \"__main__\"' pattern prevents code from")
print("running when the module is imported.")
# -----------------------------------------------------------------------------
# 3. Module Structure Example
# -----------------------------------------------------------------------------
print("\n--- Module Structure ---")
# A well-structured module typically has:
module_template = '''
"""
Module: my_module
Description: What this module does
This module provides functionality for...
"""
# Imports at the top
import os
from typing import List
# Module-level constants
VERSION = "1.0.0"
DEFAULT_TIMEOUT = 30
# Private helpers (convention: prefix with _)
def _internal_helper():
"""Not meant to be used outside this module."""
pass
# Public functions
def public_function(arg1: str) -> str:
"""
This is a public function.
Args:
arg1: Description of argument
Returns:
Description of return value
"""
return f"Result: {arg1}"
# Classes
class MyClass:
"""A class in the module."""
pass
# Main entry point (if applicable)
def main():
"""Entry point when run as script."""
print(public_function("test"))
if __name__ == "__main__":
main()
'''
print("A module should have:")
print(" 1. Module docstring at top")
print(" 2. Imports")
print(" 3. Constants")
print(" 4. Private helpers (prefixed with _)")
print(" 5. Public functions/classes")
print(" 6. Main block if it's executable")
# -----------------------------------------------------------------------------
# 4. Creating a Package
# -----------------------------------------------------------------------------
print("\n--- Creating a Package ---")
# A package is a directory containing:
# - __init__.py (can be empty)
# - One or more module files
package_structure = """
my_package/
__init__.py
module1.py
module2.py
utils/
__init__.py
helpers.py
validators.py
"""
print("Package structure:")
print(package_structure)
# The __init__.py file makes a directory a package
# It can be empty or contain initialization code
init_example = '''
# my_package/__init__.py
# Package version
__version__ = "1.0.0"
# Control what gets imported with "from my_package import *"
__all__ = ['module1', 'module2', 'important_function']
# Import commonly used items for convenient access
from .module1 import important_function
from .module2 import AnotherClass
'''
print("__init__.py can:")
print(" - Define package version")
print(" - Control __all__ exports")
print(" - Import frequently used items for convenience")
# -----------------------------------------------------------------------------
# 5. Import Examples for Packages
# -----------------------------------------------------------------------------
print("\n--- Package Import Examples ---")
import_examples = """
# Given this package structure:
# myapp/
# __init__.py
# core.py
# utils/
# __init__.py
# helpers.py
# Import the package
import myapp
# Import a module from the package
from myapp import core
# Import a function from a module
from myapp.core import process_data
# Import from nested package
from myapp.utils import helpers
from myapp.utils.helpers import format_output
# Relative imports (inside the package)
# In myapp/core.py:
from . import utils # Same-level import
from .utils import helpers # Nested module
from .utils.helpers import format_output
from .. import other_package # Parent-level (if applicable)
"""
print("Package import patterns:")
print(" import package")
print(" from package import module")
print(" from package.module import function")
print(" from package.sub import submodule")
# -----------------------------------------------------------------------------
# 6. Simulating a Module
# -----------------------------------------------------------------------------
print("\n--- Simulated Module Example ---")
# Let's create module-like code inline to demonstrate
# === This would be in: calculator.py ===
class Calculator:
"""A simple calculator class."""
def __init__(self):
self.history = []
def add(self, a, b):
"""Add two numbers."""
result = a + b
self.history.append(f"{a} + {b} = {result}")
return result
def subtract(self, a, b):
"""Subtract b from a."""
result = a - b
self.history.append(f"{a} - {b} = {result}")
return result
def get_history(self):
"""Get calculation history."""
return self.history
# === Using the "module" ===
calc = Calculator()
print(f"5 + 3 = {calc.add(5, 3)}")
print(f"10 - 4 = {calc.subtract(10, 4)}")
print(f"History: {calc.get_history()}")
# -----------------------------------------------------------------------------
# 7. Module Documentation
# -----------------------------------------------------------------------------
print("\n--- Module Documentation ---")
# Good module documentation includes:
# - Module-level docstring
# - Function/class docstrings
# - Type hints
# - Examples
documented_module = '''
"""
mymodule - A demonstration of proper documentation
This module provides utilities for string manipulation
and validation. It follows Google-style docstrings.
Example:
>>> from mymodule import validate_email
>>> validate_email("test@example.com")
True
Attributes:
EMAIL_REGEX (str): Regular expression for email validation
"""
import re
from typing import Optional
EMAIL_REGEX = r'^[\w.-]+@[\w.-]+\.\w+$'
def validate_email(email: str) -> bool:
"""
Validate an email address format.
Args:
email: The email address to validate
Returns:
True if valid format, False otherwise
Raises:
TypeError: If email is not a string
Example:
>>> validate_email("user@domain.com")
True
>>> validate_email("invalid-email")
False
"""
if not isinstance(email, str):
raise TypeError("email must be a string")
return bool(re.match(EMAIL_REGEX, email))
'''
print("Include in your modules:")
print(" - Module docstring explaining purpose")
print(" - Type hints for parameters and returns")
print(" - Examples in docstrings")
print(" - Proper exception documentation")
# -----------------------------------------------------------------------------
# 8. The __all__ Variable
# -----------------------------------------------------------------------------
print("\n--- The __all__ Variable ---")
# __all__ controls what's exported with "from module import *"
all_example = '''
# mymodule.py
# Only these will be exported with "from mymodule import *"
__all__ = ['public_func', 'PublicClass', 'CONSTANT']
CONSTANT = 42
def public_func():
"""This is meant to be used externally."""
pass
def _private_func():
"""This is internal (won't be exported)."""
pass
class PublicClass:
"""This class is for external use."""
pass
'''
print("__all__ defines what gets exported with 'import *'")
print("Items NOT in __all__ won't be exported")
print("Underscore-prefixed items are convention for private")
# -----------------------------------------------------------------------------
# 9. Reloading Modules
# -----------------------------------------------------------------------------
print("\n--- Reloading Modules ---")
# During development, you might need to reload a modified module
from importlib import reload
# If you modify 'mymodule', you can reload it:
# import mymodule
# # ... modify the file ...
# reload(mymodule) # Get the updated version
print("Use importlib.reload() to reload a modified module")
print("Useful during development and debugging")
# -----------------------------------------------------------------------------
# 10. Project Structure Best Practices
# -----------------------------------------------------------------------------
print("\n--- Project Structure Best Practices ---")
project_structure = """
myproject/
README.md
setup.py or pyproject.toml
requirements.txt
.gitignore
src/
mypackage/
__init__.py
core.py
utils.py
models/
__init__.py
user.py
product.py
tests/
__init__.py
test_core.py
test_utils.py
docs/
index.md
scripts/
run_server.py
"""
print("Recommended project structure:")
print(project_structure)
print("Key points:")
print(" - Separate source code (src/) from tests")
print(" - Keep configuration at project root")
print(" - Use __init__.py for all packages")
print(" - Tests mirror source structure")

View File

@ -0,0 +1,335 @@
"""
================================================================================
File: 01_try_except.py
Topic: Exception Handling with try-except
================================================================================
This file demonstrates error handling in Python using try-except blocks.
Proper error handling makes your code more robust and user-friendly by
gracefully managing unexpected situations.
Key Concepts:
- try, except, else, finally blocks
- Catching specific exceptions
- Exception hierarchy
- Raising exceptions
- Getting exception information
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic try-except
# -----------------------------------------------------------------------------
# Catch and handle errors that would otherwise crash your program
print("--- Basic try-except ---")
# Without error handling - this would crash:
# result = 10 / 0 # ZeroDivisionError!
# With error handling:
try:
result = 10 / 0
except:
print("An error occurred!")
# Better - catch specific exception:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = 0
print(f"Result: {result}")
# -----------------------------------------------------------------------------
# 2. Catching Specific Exceptions
# -----------------------------------------------------------------------------
# Different errors need different handling
print("\n--- Catching Specific Exceptions ---")
def safe_divide(a, b):
"""Divide with specific error handling."""
try:
result = a / b
except ZeroDivisionError:
print(" Error: Cannot divide by zero")
return None
except TypeError:
print(" Error: Invalid types for division")
return None
return result
print(f"10 / 2 = {safe_divide(10, 2)}")
print(f"10 / 0 = {safe_divide(10, 0)}")
print(f"10 / 'a' = {safe_divide(10, 'a')}")
# -----------------------------------------------------------------------------
# 3. Catching Multiple Exceptions
# -----------------------------------------------------------------------------
print("\n--- Multiple Exceptions ---")
# Catch multiple in one line
def process_number(value):
"""Process a number with multiple exception handlers."""
try:
# This might raise ValueError or TypeError
number = int(value)
result = 100 / number
return result
except (ValueError, TypeError) as e:
print(f" Conversion error: {e}")
return None
except ZeroDivisionError:
print(" Cannot divide by zero")
return None
print(f"process_number('5'): {process_number('5')}")
print(f"process_number('abc'): {process_number('abc')}")
print(f"process_number('0'): {process_number('0')}")
# -----------------------------------------------------------------------------
# 4. The else Clause
# -----------------------------------------------------------------------------
# Runs only if no exception occurred
print("\n--- The else Clause ---")
def divide_with_else(a, b):
"""Division with else clause for success logging."""
try:
result = a / b
except ZeroDivisionError:
print(" Division failed: Cannot divide by zero")
return None
else:
# Only runs if try block succeeded
print(f" Division successful: {a} / {b} = {result}")
return result
divide_with_else(10, 2)
divide_with_else(10, 0)
# -----------------------------------------------------------------------------
# 5. The finally Clause
# -----------------------------------------------------------------------------
# Always runs, regardless of whether an exception occurred
print("\n--- The finally Clause ---")
def read_file_example(filename):
"""Demonstrate finally for cleanup."""
file = None
try:
print(f" Attempting to open '{filename}'...")
# Simulating file operation
if filename == "missing.txt":
raise FileNotFoundError("File not found")
print(" File opened successfully!")
return "File content"
except FileNotFoundError as e:
print(f" Error: {e}")
return None
finally:
# This ALWAYS runs
print(" Cleanup: Closing file (if open)")
read_file_example("data.txt")
print()
read_file_example("missing.txt")
# Real-world pattern with file
print("\n--- Real file handling ---")
# Best practice: use 'with' statement (handles cleanup automatically)
# But for learning, here's the try-finally pattern:
"""
file = None
try:
file = open("data.txt", "r")
content = file.read()
finally:
if file:
file.close() # Always closes, even if error occurred
"""
# -----------------------------------------------------------------------------
# 6. Complete try-except-else-finally
# -----------------------------------------------------------------------------
print("\n--- Complete Pattern ---")
def complete_example(value):
"""Show all four clauses in action."""
print(f"\n Processing: {value}")
try:
number = int(value)
result = 100 / number
except ValueError:
print(" EXCEPT: Not a valid integer")
result = None
except ZeroDivisionError:
print(" EXCEPT: Division by zero")
result = None
else:
print(f" ELSE: Success! Result = {result}")
finally:
print(" FINALLY: This always runs")
return result
complete_example("10")
complete_example("abc")
complete_example("0")
# -----------------------------------------------------------------------------
# 7. Getting Exception Information
# -----------------------------------------------------------------------------
print("\n--- Exception Information ---")
try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError as e:
print(f"Exception type: {type(e).__name__}")
print(f"Exception message: {e}")
print(f"Exception args: {e.args}")
# Getting full traceback
import traceback
try:
result = 1 / 0
except ZeroDivisionError:
print("\nFull traceback:")
traceback.print_exc()
# -----------------------------------------------------------------------------
# 8. Common Built-in Exceptions
# -----------------------------------------------------------------------------
print("\n--- Common Exceptions ---")
exceptions_demo = [
("ValueError", "int('abc')"),
("TypeError", "'2' + 2"),
("IndexError", "[1,2,3][10]"),
("KeyError", "{}['missing']"),
("AttributeError", "'string'.missing_method()"),
("FileNotFoundError", "open('nonexistent.txt')"),
("ZeroDivisionError", "1/0"),
("ImportError", "import nonexistent_module"),
("NameError", "undefined_variable"),
]
print("Common exception types:")
for name, example in exceptions_demo:
print(f" {name}: {example}")
# -----------------------------------------------------------------------------
# 9. Exception Hierarchy
# -----------------------------------------------------------------------------
print("\n--- Exception Hierarchy ---")
# All exceptions inherit from BaseException
# Most use Exception as base
hierarchy = """
BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
StopIteration
ArithmeticError
ZeroDivisionError
FloatingPointError
OverflowError
LookupError
IndexError
KeyError
ValueError
TypeError
AttributeError
OSError
FileNotFoundError
PermissionError
RuntimeError
"""
print("Exception hierarchy (simplified):")
print(hierarchy)
# Catching parent catches all children
try:
result = 1 / 0
except ArithmeticError: # Catches ZeroDivisionError too!
print("Caught an arithmetic error (parent class)")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Example 1: User input validation
def get_positive_number(prompt):
"""Get a positive number from user (simulated)."""
test_inputs = ["abc", "-5", "0", "10"]
for user_input in test_inputs:
print(f" Input: '{user_input}'", end="")
try:
number = float(user_input)
if number <= 0:
raise ValueError("Number must be positive")
print(f"Valid! ({number})")
return number
except ValueError as e:
print(f"Invalid: {e}")
return None
print("User input validation:")
get_positive_number("Enter a positive number: ")
# Example 2: Safe dictionary access
def safe_get(dictionary, *keys, default=None):
"""Safely navigate nested dictionary."""
current = dictionary
try:
for key in keys:
current = current[key]
return current
except (KeyError, TypeError):
return default
data = {"user": {"profile": {"name": "Alice"}}}
print(f"\nNested access: {safe_get(data, 'user', 'profile', 'name')}")
print(f"Missing key: {safe_get(data, 'user', 'missing', 'name', default='N/A')}")
# Example 3: Retry pattern
def fetch_with_retry(max_retries=3):
"""Simulate fetching with retry on failure."""
import random
for attempt in range(1, max_retries + 1):
try:
print(f" Attempt {attempt}...", end=" ")
# Simulate random failure
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError("Network error")
print("Success!")
return "Data"
except ConnectionError as e:
print(f"Failed: {e}")
if attempt == max_retries:
print(" All retries exhausted!")
return None
print("\nRetry pattern:")
fetch_with_retry()

View File

@ -0,0 +1,408 @@
"""
================================================================================
File: 02_custom_exceptions.py
Topic: Creating and Using Custom Exceptions
================================================================================
This file demonstrates how to create your own exception classes in Python.
Custom exceptions make your code more expressive and allow you to handle
specific error conditions in a meaningful way.
Key Concepts:
- Why use custom exceptions
- Creating exception classes
- Exception with custom attributes
- Exception chaining
- Best practices
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Why Custom Exceptions?
# -----------------------------------------------------------------------------
print("--- Why Custom Exceptions? ---")
# Built-in exceptions are generic
# Custom exceptions:
# - Are more descriptive
# - Can carry additional data
# - Allow specific handling
# - Document error conditions
print("Benefits of custom exceptions:")
print(" - More meaningful error messages")
print(" - Carry context-specific data")
print(" - Enable targeted exception handling")
print(" - Self-documenting code")
# -----------------------------------------------------------------------------
# 2. Basic Custom Exception
# -----------------------------------------------------------------------------
print("\n--- Basic Custom Exception ---")
# Inherit from Exception (or a more specific built-in)
class InvalidAgeError(Exception):
"""Raised when an age value is invalid."""
pass
# Using the custom exception
def set_age(age):
"""Set age with validation."""
if age < 0:
raise InvalidAgeError("Age cannot be negative")
if age > 150:
raise InvalidAgeError("Age seems unrealistically high")
return age
# Catching custom exception
try:
set_age(-5)
except InvalidAgeError as e:
print(f"Caught InvalidAgeError: {e}")
try:
set_age(200)
except InvalidAgeError as e:
print(f"Caught InvalidAgeError: {e}")
# -----------------------------------------------------------------------------
# 3. Custom Exception with Attributes
# -----------------------------------------------------------------------------
print("\n--- Exception with Custom Attributes ---")
class ValidationError(Exception):
"""Raised when validation fails."""
def __init__(self, field, value, message):
self.field = field
self.value = value
self.message = message
# Call parent constructor with full message
super().__init__(f"{field}: {message} (got: {value})")
def to_dict(self):
"""Convert error to dictionary (useful for API responses)."""
return {
"field": self.field,
"value": self.value,
"message": self.message
}
# Using the enhanced exception
def validate_email(email):
"""Validate email format."""
if not email:
raise ValidationError("email", email, "Email is required")
if "@" not in email:
raise ValidationError("email", email, "Invalid email format")
return True
try:
validate_email("invalid-email")
except ValidationError as e:
print(f"Error: {e}")
print(f"Field: {e.field}")
print(f"Value: {e.value}")
print(f"As dict: {e.to_dict()}")
# -----------------------------------------------------------------------------
# 4. Exception Hierarchy
# -----------------------------------------------------------------------------
print("\n--- Exception Hierarchy ---")
# Create a base exception for your module/application
class AppError(Exception):
"""Base exception for the application."""
pass
class DatabaseError(AppError):
"""Database-related errors."""
pass
class ConnectionError(DatabaseError):
"""Database connection errors."""
pass
class QueryError(DatabaseError):
"""Query execution errors."""
pass
class AuthenticationError(AppError):
"""Authentication-related errors."""
pass
class PermissionError(AppError):
"""Permission-related errors."""
pass
# Now you can catch broadly or specifically
def database_operation():
raise QueryError("Invalid SQL syntax")
try:
database_operation()
except DatabaseError as e:
# Catches ConnectionError and QueryError
print(f"Database error: {e}")
except AppError as e:
# Catches all app errors
print(f"App error: {e}")
# -----------------------------------------------------------------------------
# 5. Exception Chaining
# -----------------------------------------------------------------------------
print("\n--- Exception Chaining ---")
class ProcessingError(Exception):
"""Error during data processing."""
pass
def parse_config(data):
"""Parse configuration data."""
try:
# Simulating a parsing error
if not data:
raise ValueError("Empty data")
return {"parsed": data}
except ValueError as e:
# Chain the original exception
raise ProcessingError("Failed to parse config") from e
try:
parse_config("")
except ProcessingError as e:
print(f"Processing error: {e}")
print(f"Caused by: {e.__cause__}")
# Using 'from None' to suppress chaining
def simple_error():
try:
raise ValueError("original")
except ValueError:
raise RuntimeError("new error") from None # Hides original
# -----------------------------------------------------------------------------
# 6. Practical Example: User Registration
# -----------------------------------------------------------------------------
print("\n--- Practical Example: User Registration ---")
class RegistrationError(Exception):
"""Base class for registration errors."""
pass
class UsernameError(RegistrationError):
"""Username-related errors."""
def __init__(self, username, reason):
self.username = username
self.reason = reason
super().__init__(f"Username '{username}': {reason}")
class PasswordError(RegistrationError):
"""Password-related errors."""
def __init__(self, issues):
self.issues = issues
super().__init__(f"Password issues: {', '.join(issues)}")
class EmailError(RegistrationError):
"""Email-related errors."""
pass
def validate_username(username):
"""Validate username."""
if len(username) < 3:
raise UsernameError(username, "Too short (min 3 chars)")
if not username.isalnum():
raise UsernameError(username, "Must be alphanumeric")
# Check if username exists (simulated)
existing = ["admin", "root", "user"]
if username.lower() in existing:
raise UsernameError(username, "Already taken")
return True
def validate_password(password):
"""Validate password strength."""
issues = []
if len(password) < 8:
issues.append("too short")
if not any(c.isupper() for c in password):
issues.append("needs uppercase")
if not any(c.islower() for c in password):
issues.append("needs lowercase")
if not any(c.isdigit() for c in password):
issues.append("needs digit")
if issues:
raise PasswordError(issues)
return True
def register_user(username, password, email):
"""Register a new user."""
try:
validate_username(username)
validate_password(password)
# validate_email(email) - already defined above
print(f" ✓ User '{username}' registered successfully!")
return True
except RegistrationError as e:
print(f" ✗ Registration failed: {e}")
return False
# Test registration
print("Registration attempts:")
register_user("ab", "password123", "test@example.com")
register_user("admin", "Password1", "test@example.com")
register_user("newuser", "weak", "test@example.com")
register_user("newuser", "StrongPass123", "test@example.com")
# -----------------------------------------------------------------------------
# 7. Context Manager with Exceptions
# -----------------------------------------------------------------------------
print("\n--- Exception in Context Manager ---")
class ManagedResource:
"""Resource with cleanup that handles exceptions."""
def __init__(self, name):
self.name = name
def __enter__(self):
print(f" Acquiring resource: {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f" Releasing resource: {self.name}")
if exc_type is not None:
print(f" Exception occurred: {exc_type.__name__}: {exc_val}")
# Return True to suppress the exception
# Return False to propagate it
return False # Don't suppress exceptions
# Using the context manager
print("Context manager with exception:")
try:
with ManagedResource("database") as resource:
print(f" Using {resource.name}")
raise RuntimeError("Something went wrong!")
except RuntimeError:
print(" Caught exception outside")
# -----------------------------------------------------------------------------
# 8. Re-raising Exceptions
# -----------------------------------------------------------------------------
print("\n--- Re-raising Exceptions ---")
def process_data(data):
"""Process data with logging and re-raise."""
try:
# Processing that might fail
if not data:
raise ValueError("Empty data")
return data.upper()
except ValueError as e:
print(f" Logging error: {e}")
raise # Re-raise the same exception
try:
process_data("")
except ValueError as e:
print(f"Caught re-raised exception: {e}")
# -----------------------------------------------------------------------------
# 9. Exception Documentation
# -----------------------------------------------------------------------------
print("\n--- Exception Documentation ---")
class APIError(Exception):
"""
Base exception for API errors.
Attributes:
status_code: HTTP status code
message: Human-readable error message
error_code: Application-specific error code
Example:
>>> raise APIError(404, "Resource not found", "NOT_FOUND")
"""
def __init__(self, status_code: int, message: str, error_code: str = None):
"""
Initialize API error.
Args:
status_code: HTTP status code (e.g., 400, 404, 500)
message: Human-readable error description
error_code: Optional application error code
"""
self.status_code = status_code
self.message = message
self.error_code = error_code
super().__init__(message)
def to_response(self):
"""Convert to API response format."""
return {
"error": {
"code": self.error_code,
"message": self.message,
"status": self.status_code
}
}
# Using documented exception
try:
raise APIError(404, "User not found", "USER_NOT_FOUND")
except APIError as e:
print(f"API Error Response: {e.to_response()}")
# -----------------------------------------------------------------------------
# 10. Best Practices
# -----------------------------------------------------------------------------
print("\n--- Best Practices ---")
best_practices = """
1. Inherit from Exception, not BaseException
- BaseException includes SystemExit, KeyboardInterrupt
2. Create a hierarchy for related exceptions
- Base exception for your module/app
- Specific exceptions inherit from base
3. Add useful attributes
- Include context that helps debugging
- Provide methods for serialization (API responses)
4. Document your exceptions
- What conditions trigger them
- What attributes they have
- How to handle them
5. Use meaningful names
- End with 'Error' or 'Exception'
- Be specific: UserNotFoundError, not JustError
6. Don't over-catch
- except Exception: catches too much
- Be specific about what you expect
7. Re-raise when appropriate
- Log and re-raise for debugging
- Transform to appropriate exception type
8. Use exception chaining
- Use 'from' to preserve original exception
- Helps with debugging complex systems
"""
print(best_practices)

View File

@ -0,0 +1,389 @@
"""
================================================================================
File: 01_classes_objects.py
Topic: Classes and Objects in Python
================================================================================
This file introduces Object-Oriented Programming (OOP) in Python, focusing on
classes and objects. Classes are blueprints for creating objects, which bundle
data (attributes) and functionality (methods) together.
Key Concepts:
- Defining classes
- Creating objects (instances)
- Instance attributes and methods
- Class attributes
- The self parameter
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. What is a Class?
# -----------------------------------------------------------------------------
# A class is a blueprint for creating objects
print("--- What is a Class? ---")
# Simple class definition
class Dog:
"""A simple class representing a dog."""
pass # Empty class (for now)
# Creating objects (instances) from the class
dog1 = Dog()
dog2 = Dog()
print(f"dog1 is a: {type(dog1)}")
print(f"dog1 and dog2 are different objects: {dog1 is not dog2}")
# -----------------------------------------------------------------------------
# 2. Instance Attributes
# -----------------------------------------------------------------------------
# Each object can have its own data
print("\n--- Instance Attributes ---")
class Person:
"""A class representing a person."""
def __init__(self, name, age):
"""Initialize person with name and age."""
# self refers to the instance being created
self.name = name
self.age = age
# Creating instances with attributes
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)
print(f"Person 1: {person1.name}, age {person1.age}")
print(f"Person 2: {person2.name}, age {person2.age}")
# Modifying attributes
person1.age = 26
print(f"After birthday: {person1.name} is now {person1.age}")
# Adding new attributes to instance
person1.email = "alice@example.com"
print(f"Added email: {person1.email}")
# -----------------------------------------------------------------------------
# 3. Instance Methods
# -----------------------------------------------------------------------------
# Functions defined inside a class that operate on instances
print("\n--- Instance Methods ---")
class Rectangle:
"""A class representing a rectangle."""
def __init__(self, width, height):
"""Initialize rectangle dimensions."""
self.width = width
self.height = height
def area(self):
"""Calculate the area of the rectangle."""
return self.width * self.height
def perimeter(self):
"""Calculate the perimeter of the rectangle."""
return 2 * (self.width + self.height)
def describe(self):
"""Return a description of the rectangle."""
return f"Rectangle {self.width}x{self.height}"
# Using methods
rect = Rectangle(5, 3)
print(f"Rectangle: {rect.describe()}")
print(f"Area: {rect.area()}")
print(f"Perimeter: {rect.perimeter()}")
# -----------------------------------------------------------------------------
# 4. The self Parameter
# -----------------------------------------------------------------------------
# self refers to the current instance
print("\n--- Understanding self ---")
class Counter:
"""A class demonstrating self."""
def __init__(self):
"""Initialize counter to 0."""
self.count = 0
def increment(self):
"""Increase count by 1."""
self.count += 1 # self.count refers to this instance's count
return self
def decrement(self):
"""Decrease count by 1."""
self.count -= 1
return self
def reset(self):
"""Reset count to 0."""
self.count = 0
return self
def get_count(self):
"""Return current count."""
return self.count
counter1 = Counter()
counter2 = Counter()
counter1.increment()
counter1.increment()
counter1.increment()
counter2.increment()
print(f"Counter 1: {counter1.get_count()}") # 3
print(f"Counter 2: {counter2.get_count()}") # 1 - separate instance!
# Method chaining (returning self)
counter1.reset().increment().increment()
print(f"After chaining: {counter1.get_count()}")
# -----------------------------------------------------------------------------
# 5. Class Attributes
# -----------------------------------------------------------------------------
# Attributes shared by all instances of a class
print("\n--- Class Attributes ---")
class Car:
"""A class with class attributes."""
# Class attribute - shared by all instances
wheels = 4
count = 0 # Track number of cars created
def __init__(self, brand, model):
"""Initialize car with brand and model."""
self.brand = brand # Instance attribute
self.model = model # Instance attribute
Car.count += 1 # Increment class attribute
def describe(self):
"""Describe the car."""
return f"{self.brand} {self.model} ({Car.wheels} wheels)"
# Creating cars
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
car3 = Car("Ford", "Mustang")
print(f"Car 1: {car1.describe()}")
print(f"Car 2: {car2.describe()}")
print(f"Total cars created: {Car.count}")
# Accessing class attribute from class or instance
print(f"Car.wheels: {Car.wheels}")
print(f"car1.wheels: {car1.wheels}")
# Modifying class attribute
Car.wheels = 6 # Now all cars have 6 wheels
print(f"After modification - car1.wheels: {car1.wheels}")
print(f"After modification - car2.wheels: {car2.wheels}")
# -----------------------------------------------------------------------------
# 6. Private Attributes (Convention)
# -----------------------------------------------------------------------------
# Python uses naming conventions for privacy
print("\n--- Private Attributes ---")
class BankAccount:
"""A class demonstrating private attributes."""
def __init__(self, owner, balance=0):
"""Initialize bank account."""
self.owner = owner # Public
self._balance = balance # Protected (convention)
self.__id = id(self) # Private (name mangling)
def deposit(self, amount):
"""Deposit money into account."""
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount):
"""Withdraw money from account."""
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
def get_balance(self):
"""Get current balance."""
return self._balance
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(f"Account owner: {account.owner}")
print(f"Balance: {account.get_balance()}")
# Can still access protected (but shouldn't)
print(f"Protected _balance: {account._balance}")
# Mangled name for private
print(f"Mangled private __id: {account._BankAccount__id}")
# -----------------------------------------------------------------------------
# 7. Methods that Return New Objects
# -----------------------------------------------------------------------------
print("\n--- Returning Objects ---")
class Point:
"""A class representing a 2D point."""
def __init__(self, x, y):
"""Initialize point coordinates."""
self.x = x
self.y = y
def move(self, dx, dy):
"""Return a new point moved by (dx, dy)."""
return Point(self.x + dx, self.y + dy)
def distance_to(self, other):
"""Calculate distance to another point."""
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
def __str__(self):
"""String representation."""
return f"Point({self.x}, {self.y})"
p1 = Point(0, 0)
p2 = p1.move(3, 4)
print(f"Original point: {p1}")
print(f"New point: {p2}")
print(f"Distance: {p1.distance_to(p2)}")
# -----------------------------------------------------------------------------
# 8. Special Methods (Dunder Methods)
# -----------------------------------------------------------------------------
# Methods with double underscores have special meanings
print("\n--- Special Methods ---")
class Vector:
"""A class demonstrating special methods."""
def __init__(self, x, y):
"""Initialize vector."""
self.x = x
self.y = y
def __str__(self):
"""Human-readable string representation."""
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""Developer representation (for debugging)."""
return f"Vector(x={self.x}, y={self.y})"
def __len__(self):
"""Length (in this case, magnitude as int)."""
return int((self.x ** 2 + self.y ** 2) ** 0.5)
def __add__(self, other):
"""Add two vectors."""
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
"""Check equality."""
return self.x == other.x and self.y == other.y
v1 = Vector(3, 4)
v2 = Vector(1, 2)
v3 = v1 + v2 # Uses __add__
print(f"v1: {v1}") # Uses __str__
print(f"v1 + v2 = {v3}") # Uses __add__ and __str__
print(f"len(v1): {len(v1)}") # Uses __len__
print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}") # Uses __eq__
# -----------------------------------------------------------------------------
# 9. Practical Example: Student Class
# -----------------------------------------------------------------------------
print("\n--- Practical Example: Student ---")
class Student:
"""A class representing a student."""
school_name = "Python Academy" # Class attribute
def __init__(self, name, student_id):
"""Initialize student."""
self.name = name
self.student_id = student_id
self.grades = []
def add_grade(self, grade):
"""Add a grade (0-100)."""
if 0 <= grade <= 100:
self.grades.append(grade)
return True
return False
def get_average(self):
"""Calculate grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
def get_letter_grade(self):
"""Get letter grade based on average."""
avg = self.get_average()
if avg >= 90: return 'A'
if avg >= 80: return 'B'
if avg >= 70: return 'C'
if avg >= 60: return 'D'
return 'F'
def __str__(self):
"""String representation."""
return f"{self.name} (ID: {self.student_id}) - {self.school_name}"
# Using the Student class
student = Student("Baraa", "S12345")
student.add_grade(85)
student.add_grade(90)
student.add_grade(78)
print(student)
print(f"Grades: {student.grades}")
print(f"Average: {student.get_average():.1f}")
print(f"Letter Grade: {student.get_letter_grade()}")
# -----------------------------------------------------------------------------
# 10. Summary
# -----------------------------------------------------------------------------
print("\n--- Summary ---")
summary = """
Key OOP concepts:
- Class: Blueprint for objects
- Object: Instance of a class
- self: Reference to current instance
- __init__: Constructor method
- Instance attributes: Unique to each object
- Class attributes: Shared by all instances
- Methods: Functions that belong to a class
- Special methods: __str__, __repr__, __add__, etc.
"""
print(summary)

446
08_oop/02_init_methods.py Normal file
View File

@ -0,0 +1,446 @@
"""
================================================================================
File: 02_init_methods.py
Topic: Constructors and Initialization in Python
================================================================================
This file explores the __init__ method and other initialization patterns
in Python classes. The __init__ method is called when creating new objects
and sets up the initial state of the instance.
Key Concepts:
- The __init__ constructor
- Required vs optional parameters
- Default values
- Property decorators
- Alternative constructors (classmethod)
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic __init__ Method
# -----------------------------------------------------------------------------
# __init__ initializes newly created objects
print("--- Basic __init__ ---")
class Book:
"""A class representing a book."""
def __init__(self, title, author):
"""
Initialize a new book.
Args:
title: The book's title
author: The book's author
"""
self.title = title
self.author = author
# Creating instances calls __init__ automatically
book = Book("1984", "George Orwell")
print(f"Book: '{book.title}' by {book.author}")
# Without proper arguments, you get an error:
# book = Book() # TypeError: missing 2 required positional arguments
# -----------------------------------------------------------------------------
# 2. Default Parameter Values
# -----------------------------------------------------------------------------
# Make some parameters optional
print("\n--- Default Values ---")
class User:
"""A class with default parameter values."""
def __init__(self, username, email=None, role="user", active=True):
"""
Initialize a new user.
Args:
username: Required username
email: Optional email address
role: User role (default: "user")
active: Is user active (default: True)
"""
self.username = username
self.email = email
self.role = role
self.active = active
def __str__(self):
email = self.email or "No email"
status = "Active" if self.active else "Inactive"
return f"{self.username} ({self.role}) - {email} - {status}"
# Various ways to create users
user1 = User("alice")
user2 = User("bob", "bob@example.com")
user3 = User("charlie", "charlie@example.com", "admin")
user4 = User("dave", active=False)
print(user1)
print(user2)
print(user3)
print(user4)
# -----------------------------------------------------------------------------
# 3. Validation in __init__
# -----------------------------------------------------------------------------
# Validate and process data during initialization
print("\n--- Validation in __init__ ---")
class Temperature:
"""A class with validation in __init__."""
def __init__(self, celsius):
"""
Initialize temperature in Celsius.
Args:
celsius: Temperature in Celsius
Raises:
ValueError: If temperature is below absolute zero
"""
if celsius < -273.15:
raise ValueError("Temperature cannot be below absolute zero!")
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
@property
def kelvin(self):
"""Get temperature in Kelvin."""
return self._celsius + 273.15
temp = Temperature(25)
print(f"Temperature: {temp.celsius}°C = {temp.fahrenheit}°F = {temp.kelvin}K")
# Validation in action
try:
invalid_temp = Temperature(-300)
except ValueError as e:
print(f"Validation error: {e}")
# -----------------------------------------------------------------------------
# 4. Mutable Default Arguments Warning
# -----------------------------------------------------------------------------
# Never use mutable objects as default arguments!
print("\n--- Mutable Defaults Warning ---")
# BAD - mutable default argument
class BadStudent:
def __init__(self, name, grades=[]): # DON'T DO THIS!
self.name = name
self.grades = grades
s1 = BadStudent("Alice")
s1.grades.append(90)
s2 = BadStudent("Bob")
print(f"Bob's grades (should be empty): {s2.grades}") # Contains 90!
# GOOD - use None and create new list
class GoodStudent:
def __init__(self, name, grades=None):
self.name = name
self.grades = grades if grades is not None else []
g1 = GoodStudent("Carol")
g1.grades.append(85)
g2 = GoodStudent("Dan")
print(f"Dan's grades (correctly empty): {g2.grades}")
# -----------------------------------------------------------------------------
# 5. Property Decorators
# -----------------------------------------------------------------------------
# Control attribute access with getters and setters
print("\n--- Property Decorators ---")
class Circle:
"""A circle with property decorators."""
def __init__(self, radius):
"""Initialize circle with radius."""
self._radius = radius # Protected attribute
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def diameter(self):
"""Get the diameter (read-only computed property)."""
return self._radius * 2
@property
def area(self):
"""Get the area (read-only computed property)."""
import math
return math.pi * self._radius ** 2
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Diameter: {circle.diameter}")
print(f"Area: {circle.area:.2f}")
# Using setter
circle.radius = 10
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area:.2f}")
# Validation in setter
try:
circle.radius = -5
except ValueError as e:
print(f"Setter validation: {e}")
# -----------------------------------------------------------------------------
# 6. Alternative Constructors with @classmethod
# -----------------------------------------------------------------------------
# Create objects in different ways
print("\n--- Alternative Constructors ---")
class Person:
"""A class with alternative constructors."""
def __init__(self, first_name, last_name, age):
"""Standard constructor."""
self.first_name = first_name
self.last_name = last_name
self.age = age
@classmethod
def from_birth_year(cls, first_name, last_name, birth_year):
"""Create person from birth year instead of age."""
import datetime
age = datetime.datetime.now().year - birth_year
return cls(first_name, last_name, age)
@classmethod
def from_full_name(cls, full_name, age):
"""Create person from full name string."""
parts = full_name.split()
first_name = parts[0]
last_name = parts[-1] if len(parts) > 1 else ""
return cls(first_name, last_name, age)
@classmethod
def from_dict(cls, data):
"""Create person from dictionary."""
return cls(
data.get('first_name', ''),
data.get('last_name', ''),
data.get('age', 0)
)
def __str__(self):
return f"{self.first_name} {self.last_name}, age {self.age}"
# Using different constructors
p1 = Person("John", "Doe", 30)
p2 = Person.from_birth_year("Jane", "Smith", 1995)
p3 = Person.from_full_name("Bob Johnson", 25)
p4 = Person.from_dict({"first_name": "Alice", "last_name": "Williams", "age": 28})
print("Created using different constructors:")
print(f" Standard: {p1}")
print(f" From birth year: {p2}")
print(f" From full name: {p3}")
print(f" From dict: {p4}")
# -----------------------------------------------------------------------------
# 7. __new__ vs __init__
# -----------------------------------------------------------------------------
# __new__ creates the instance, __init__ initializes it
print("\n--- __new__ vs __init__ ---")
class Singleton:
"""A singleton class using __new__."""
_instance = None
def __new__(cls):
"""Create instance only if one doesn't exist."""
if cls._instance is None:
print(" Creating new instance...")
cls._instance = super().__new__(cls)
else:
print(" Returning existing instance...")
return cls._instance
def __init__(self):
"""Initialize (called every time)."""
pass
# Both variables point to the same instance
print("Creating s1:")
s1 = Singleton()
print("Creating s2:")
s2 = Singleton()
print(f"Same instance? {s1 is s2}")
# -----------------------------------------------------------------------------
# 8. Initialization with Inheritance
# -----------------------------------------------------------------------------
print("\n--- Initialization with Inheritance ---")
class Animal:
"""Base class for animals."""
def __init__(self, name, species):
"""Initialize animal."""
self.name = name
self.species = species
def speak(self):
"""Make a sound."""
return "Some sound"
class Dog(Animal):
"""Dog class inheriting from Animal."""
def __init__(self, name, breed):
"""Initialize dog with name and breed."""
# Call parent's __init__
super().__init__(name, species="Canis familiaris")
self.breed = breed
def speak(self):
"""Dogs bark."""
return "Woof!"
class Cat(Animal):
"""Cat class inheriting from Animal."""
def __init__(self, name, indoor=True):
"""Initialize cat."""
super().__init__(name, species="Felis catus")
self.indoor = indoor
def speak(self):
"""Cats meow."""
return "Meow!"
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", indoor=True)
print(f"Dog: {dog.name} ({dog.breed}) says {dog.speak()}")
print(f"Cat: {cat.name} (Indoor: {cat.indoor}) says {cat.speak()}")
# -----------------------------------------------------------------------------
# 9. Complex Initialization Example
# -----------------------------------------------------------------------------
print("\n--- Complex Initialization ---")
from datetime import datetime
class Order:
"""A class with complex initialization."""
_order_counter = 0
def __init__(self, customer_name, items=None, discount=0):
"""
Initialize an order.
Args:
customer_name: Name of the customer
items: List of (item_name, price, quantity) tuples
discount: Discount percentage (0-100)
"""
# Auto-generate order ID
Order._order_counter += 1
self.order_id = f"ORD-{Order._order_counter:04d}"
# Set basic attributes
self.customer_name = customer_name
self.items = items or []
self.discount = max(0, min(100, discount)) # Clamp to 0-100
# Auto-generated attributes
self.created_at = datetime.now()
self._subtotal = None # Cached calculation
def add_item(self, name, price, quantity=1):
"""Add an item to the order."""
self.items.append((name, price, quantity))
self._subtotal = None # Invalidate cache
@property
def subtotal(self):
"""Calculate subtotal."""
if self._subtotal is None:
self._subtotal = sum(price * qty for _, price, qty in self.items)
return self._subtotal
@property
def total(self):
"""Calculate total with discount."""
return self.subtotal * (1 - self.discount / 100)
def __str__(self):
return f"Order {self.order_id} for {self.customer_name}: ${self.total:.2f}"
# Create orders
order1 = Order("Alice")
order1.add_item("Widget", 9.99, 2)
order1.add_item("Gadget", 14.99, 1)
order2 = Order("Bob", discount=10)
order2.add_item("Premium Widget", 29.99, 1)
print(order1)
print(f" Subtotal: ${order1.subtotal:.2f}")
print(f" Total: ${order1.total:.2f}")
print(order2)
print(f" Subtotal: ${order2.subtotal:.2f}")
print(f" Total (10% off): ${order2.total:.2f}")
# -----------------------------------------------------------------------------
# 10. Summary
# -----------------------------------------------------------------------------
print("\n--- Summary ---")
summary = """
Initialization patterns:
- __init__: Main constructor, initializes attributes
- Default values: Make parameters optional
- Validation: Raise errors for invalid input
- Properties: Computed/validated attributes
- @classmethod: Alternative constructors
- __new__: Instance creation (before __init__)
- super().__init__(): Call parent constructor
"""
print(summary)

436
08_oop/03_inheritance.py Normal file
View File

@ -0,0 +1,436 @@
"""
================================================================================
File: 03_inheritance.py
Topic: Inheritance in Python
================================================================================
This file demonstrates inheritance in Python, a fundamental OOP concept that
allows classes to inherit attributes and methods from other classes. This
promotes code reuse and establishes relationships between classes.
Key Concepts:
- Single inheritance
- Method overriding
- super() function
- Multiple inheritance
- Method Resolution Order (MRO)
- Abstract base classes
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic Inheritance
# -----------------------------------------------------------------------------
# Child class inherits from parent class
print("--- Basic Inheritance ---")
class Animal:
"""Base class for all animals."""
def __init__(self, name):
"""Initialize animal with a name."""
self.name = name
def speak(self):
"""Make a generic sound."""
return "Some generic sound"
def describe(self):
"""Describe the animal."""
return f"I am {self.name}"
class Dog(Animal):
"""Dog class inheriting from Animal."""
pass # Inherits everything from Animal
# Dog has all Animal's methods
buddy = Dog("Buddy")
print(buddy.describe())
print(f"Says: {buddy.speak()}")
print(f"Dog is instance of Animal: {isinstance(buddy, Animal)}")
# -----------------------------------------------------------------------------
# 2. Method Overriding
# -----------------------------------------------------------------------------
# Child class can override parent methods
print("\n--- Method Overriding ---")
class Cat(Animal):
"""Cat class with overridden method."""
def speak(self):
"""Cats meow instead of generic sound."""
return "Meow!"
class Cow(Animal):
"""Cow class with overridden method."""
def speak(self):
"""Cows moo."""
return "Moo!"
# Each animal speaks differently
animals = [Dog("Rex"), Cat("Whiskers"), Cow("Bessie")]
print("Each animal speaks:")
for animal in animals:
print(f" {animal.name}: {animal.speak()}")
# -----------------------------------------------------------------------------
# 3. Extending Parent Class with super()
# -----------------------------------------------------------------------------
# Use super() to call parent methods
print("\n--- Using super() ---")
class Bird(Animal):
"""Bird class extending Animal."""
def __init__(self, name, can_fly=True):
"""Initialize bird with flying ability."""
super().__init__(name) # Call parent's __init__
self.can_fly = can_fly
def speak(self):
"""Birds chirp."""
return "Chirp!"
def describe(self):
"""Extend parent's describe method."""
base = super().describe() # Get parent's description
fly_status = "can fly" if self.can_fly else "cannot fly"
return f"{base} and I {fly_status}"
sparrow = Bird("Sparrow", can_fly=True)
penguin = Bird("Penny", can_fly=False)
print(sparrow.describe())
print(penguin.describe())
# -----------------------------------------------------------------------------
# 4. Inheritance Chain
# -----------------------------------------------------------------------------
# Classes can inherit from other child classes
print("\n--- Inheritance Chain ---")
class Vehicle:
"""Base vehicle class."""
def __init__(self, brand):
self.brand = brand
def start(self):
return "Vehicle starting..."
class Car(Vehicle):
"""Car inherits from Vehicle."""
def __init__(self, brand, model):
super().__init__(brand)
self.model = model
def start(self):
return f"{self.brand} {self.model} engine starting..."
class ElectricCar(Car):
"""ElectricCar inherits from Car."""
def __init__(self, brand, model, battery_kw):
super().__init__(brand, model)
self.battery_kw = battery_kw
def start(self):
return f"{self.brand} {self.model} powering up silently... (Battery: {self.battery_kw}kW)"
def charge(self):
return f"Charging {self.battery_kw}kW battery..."
# Create instances
regular_car = Car("Toyota", "Camry")
electric_car = ElectricCar("Tesla", "Model 3", 75)
print(regular_car.start())
print(electric_car.start())
print(electric_car.charge())
# Check inheritance
print(f"\nElectricCar is Car: {isinstance(electric_car, Car)}")
print(f"ElectricCar is Vehicle: {isinstance(electric_car, Vehicle)}")
# -----------------------------------------------------------------------------
# 5. Multiple Inheritance
# -----------------------------------------------------------------------------
# A class can inherit from multiple parent classes
print("\n--- Multiple Inheritance ---")
class Flyable:
"""Mixin class for flying ability."""
def fly(self):
return f"{self.name} is flying!"
def land(self):
return f"{self.name} has landed."
class Swimmable:
"""Mixin class for swimming ability."""
def swim(self):
return f"{self.name} is swimming!"
def dive(self):
return f"{self.name} dives underwater."
class Duck(Animal, Flyable, Swimmable):
"""Duck can do everything!"""
def speak(self):
return "Quack!"
# Duck has methods from all parent classes
duck = Duck("Donald")
print(f"{duck.name} says: {duck.speak()}")
print(duck.fly())
print(duck.swim())
print(duck.describe())
# -----------------------------------------------------------------------------
# 6. Method Resolution Order (MRO)
# -----------------------------------------------------------------------------
# Python determines method lookup order
print("\n--- Method Resolution Order (MRO) ---")
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C):
pass
class E(B, C):
def method(self):
return "E"
# Check MRO
print("MRO for class D:")
for cls in D.__mro__:
print(f" {cls.__name__}")
d = D()
e = E()
print(f"\nd.method(): {d.method()}") # Returns "B" (first in MRO after D)
print(f"e.method(): {e.method()}") # Returns "E" (overridden)
# -----------------------------------------------------------------------------
# 7. Calling Methods from Specific Parent
# -----------------------------------------------------------------------------
print("\n--- Calling Specific Parent Methods ---")
class Parent1:
def greet(self):
return "Hello from Parent1"
class Parent2:
def greet(self):
return "Hello from Parent2"
class Child(Parent1, Parent2):
def greet(self):
return "Hello from Child"
def greet_all(self):
"""Call all parent greet methods."""
return [
f"Child: {self.greet()}",
f"Parent1: {Parent1.greet(self)}",
f"Parent2: {Parent2.greet(self)}"
]
child = Child()
print("Calling all greet methods:")
for greeting in child.greet_all():
print(f" {greeting}")
# -----------------------------------------------------------------------------
# 8. Abstract Base Classes
# -----------------------------------------------------------------------------
# Define interfaces that must be implemented
print("\n--- Abstract Base Classes ---")
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract base class for shapes."""
@abstractmethod
def area(self):
"""Calculate area - must be implemented."""
pass
@abstractmethod
def perimeter(self):
"""Calculate perimeter - must be implemented."""
pass
def describe(self):
"""Non-abstract method - can be used as-is."""
return f"A shape with area {self.area():.2f}"
class Rectangle(Shape):
"""Concrete rectangle class."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
"""Concrete circle class."""
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
# Cannot instantiate abstract class
# shape = Shape() # TypeError!
# Concrete classes work fine
rect = Rectangle(5, 3)
circle = Circle(4)
print(f"Rectangle: area={rect.area()}, perimeter={rect.perimeter()}")
print(f"Circle: area={circle.area():.2f}, perimeter={circle.perimeter():.2f}")
print(rect.describe())
# -----------------------------------------------------------------------------
# 9. Using isinstance() and issubclass()
# -----------------------------------------------------------------------------
print("\n--- Type Checking ---")
class Mammal(Animal):
pass
class Reptile(Animal):
pass
class DogV2(Mammal):
def speak(self):
return "Woof!"
class Snake(Reptile):
def speak(self):
return "Hiss!"
dog = DogV2("Rex")
snake = Snake("Slinky")
# isinstance() checks if object is instance of class
print("isinstance checks:")
print(f" dog isinstance of DogV2: {isinstance(dog, DogV2)}")
print(f" dog isinstance of Mammal: {isinstance(dog, Mammal)}")
print(f" dog isinstance of Animal: {isinstance(dog, Animal)}")
print(f" dog isinstance of Reptile: {isinstance(dog, Reptile)}")
# issubclass() checks class relationships
print("\nissubclass checks:")
print(f" DogV2 issubclass of Mammal: {issubclass(DogV2, Mammal)}")
print(f" DogV2 issubclass of Animal: {issubclass(DogV2, Animal)}")
print(f" DogV2 issubclass of Reptile: {issubclass(DogV2, Reptile)}")
# -----------------------------------------------------------------------------
# 10. Practical Example: Employee Hierarchy
# -----------------------------------------------------------------------------
print("\n--- Practical Example: Employee Hierarchy ---")
class Employee:
"""Base employee class."""
def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self._salary = salary
@property
def salary(self):
return self._salary
def get_annual_salary(self):
return self._salary * 12
def __str__(self):
return f"{self.name} (ID: {self.employee_id})"
class Manager(Employee):
"""Manager with team and bonus."""
def __init__(self, name, employee_id, salary, team_size=0):
super().__init__(name, employee_id, salary)
self.team_size = team_size
def get_annual_salary(self):
# Managers get bonus based on team size
base = super().get_annual_salary()
bonus = self.team_size * 1000 # $1000 per team member
return base + bonus
def __str__(self):
return f"Manager {super().__str__()} - Team: {self.team_size}"
class Developer(Employee):
"""Developer with tech stack."""
def __init__(self, name, employee_id, salary, languages):
super().__init__(name, employee_id, salary)
self.languages = languages
def get_annual_salary(self):
# Developers get extra for each language
base = super().get_annual_salary()
language_bonus = len(self.languages) * 500 # $500 per language
return base + language_bonus
def __str__(self):
langs = ", ".join(self.languages)
return f"Developer {super().__str__()} - Languages: {langs}"
# Create employees
manager = Manager("Alice", "M001", 8000, team_size=5)
dev1 = Developer("Bob", "D001", 6000, ["Python", "JavaScript", "SQL"])
dev2 = Developer("Charlie", "D002", 5500, ["Python"])
employees = [manager, dev1, dev2]
print("Employee Details:")
for emp in employees:
print(f" {emp}")
print(f" Annual Salary: ${emp.get_annual_salary():,}")

530
08_oop/04_polymorphism.py Normal file
View File

@ -0,0 +1,530 @@
"""
================================================================================
File: 04_polymorphism.py
Topic: Polymorphism in Python
================================================================================
This file demonstrates polymorphism in Python - the ability of different
objects to respond to the same method call in different ways. This is a
core OOP principle that enables flexible and extensible code.
Key Concepts:
- Duck typing
- Method polymorphism
- Operator overloading
- Protocols and ABC
- Practical polymorphism patterns
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. What is Polymorphism?
# -----------------------------------------------------------------------------
# "Many forms" - same interface, different implementations
print("--- What is Polymorphism? ---")
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Duck:
def speak(self):
return "Quack!"
# Same method call, different behavior
animals = [Dog(), Cat(), Duck()]
print("Different animals, same method:")
for animal in animals:
print(f" {type(animal).__name__}: {animal.speak()}")
# -----------------------------------------------------------------------------
# 2. Duck Typing
# -----------------------------------------------------------------------------
# "If it walks like a duck and quacks like a duck, it's a duck"
print("\n--- Duck Typing ---")
class RealDuck:
"""A real duck."""
def quack(self):
return "Quack quack!"
def fly(self):
return "Flap flap, flying!"
class RobotDuck:
"""A robot that acts like a duck."""
def quack(self):
return "Beep boop quack!"
def fly(self):
return "Propellers spinning, ascending!"
class Person:
"""A person pretending to be a duck."""
def quack(self):
return "*Person making quack sounds*"
def fly(self):
return "*Person flapping arms*"
def duck_demo(duck):
"""Demonstrate a duck (or anything duck-like)."""
print(f" {type(duck).__name__}:")
print(f" Quacking: {duck.quack()}")
print(f" Flying: {duck.fly()}")
# All these work because they have the required methods
print("Duck typing demo:")
duck_demo(RealDuck())
duck_demo(RobotDuck())
duck_demo(Person())
# -----------------------------------------------------------------------------
# 3. Method Polymorphism with Inheritance
# -----------------------------------------------------------------------------
print("\n--- Method Polymorphism ---")
class Shape:
"""Base shape class."""
def area(self):
raise NotImplementedError("Subclass must implement area()")
def describe(self):
return f"{self.__class__.__name__} with area {self.area():.2f}"
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# Polymorphism in action
shapes = [
Rectangle(5, 3),
Circle(4),
Triangle(6, 4)
]
print("Shape descriptions:")
for shape in shapes:
print(f" {shape.describe()}")
# Calculate total area polymorphically
total_area = sum(shape.area() for shape in shapes)
print(f"\nTotal area of all shapes: {total_area:.2f}")
# -----------------------------------------------------------------------------
# 4. Operator Overloading
# -----------------------------------------------------------------------------
# Same operators, different behaviors
print("\n--- Operator Overloading ---")
class Vector:
"""A 2D vector with overloaded operators."""
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __repr__(self):
return self.__str__()
def __add__(self, other):
"""Vector addition: v1 + v2"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""Vector subtraction: v1 - v2"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""Scalar multiplication: v * n"""
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""Reverse multiplication: n * v"""
return self.__mul__(scalar)
def __neg__(self):
"""Negation: -v"""
return Vector(-self.x, -self.y)
def __eq__(self, other):
"""Equality: v1 == v2"""
return self.x == other.x and self.y == other.y
def __abs__(self):
"""Magnitude: abs(v)"""
return (self.x ** 2 + self.y ** 2) ** 0.5
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 3 = {v1 * 3}")
print(f"2 * v2 = {2 * v2}")
print(f"-v1 = {-v1}")
print(f"|v1| = {abs(v1)}")
print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}")
# -----------------------------------------------------------------------------
# 5. Polymorphism with Built-in Functions
# -----------------------------------------------------------------------------
print("\n--- Built-in Function Polymorphism ---")
class Playlist:
"""A playlist that works with len() and iteration."""
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __len__(self):
"""Enable len(playlist)"""
return len(self.songs)
def __iter__(self):
"""Enable for song in playlist"""
return iter(self.songs)
def __getitem__(self, index):
"""Enable playlist[index]"""
return self.songs[index]
def __contains__(self, song):
"""Enable 'song' in playlist"""
return song in self.songs
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
print(f"Playlist '{playlist.name}':")
print(f" Length: {len(playlist)}")
print(f" First song: {playlist[0]}")
print(f" 'Song B' in playlist: {'Song B' in playlist}")
print(" All songs:")
for song in playlist:
print(f" - {song}")
# -----------------------------------------------------------------------------
# 6. Polymorphic Functions
# -----------------------------------------------------------------------------
print("\n--- Polymorphic Functions ---")
def process_payment(payment_method):
"""
Process any payment method polymorphically.
Any object with a process() method works!
"""
print(f" Processing with {type(payment_method).__name__}...")
return payment_method.process()
class CreditCard:
def __init__(self, card_number):
self.card_number = card_number[-4:] # Last 4 digits
def process(self):
return f"Charged to card ending in {self.card_number}"
class PayPal:
def __init__(self, email):
self.email = email
def process(self):
return f"Payment sent via PayPal ({self.email})"
class CryptoCurrency:
def __init__(self, wallet):
self.wallet = wallet[:8]
def process(self):
return f"Crypto transferred from {self.wallet}..."
# Same function, different payment methods
payment_methods = [
CreditCard("4111111111111234"),
PayPal("user@example.com"),
CryptoCurrency("0x1234567890abcdef")
]
print("Processing payments:")
for method in payment_methods:
result = process_payment(method)
print(f" Result: {result}")
# -----------------------------------------------------------------------------
# 7. Protocols (Informal Interfaces)
# -----------------------------------------------------------------------------
print("\n--- Protocols (Informal Interfaces) ---")
# Python 3.8+ has typing.Protocol for formal protocols
# Here's the concept with duck typing:
class Drawable:
"""Protocol: anything with a draw() method."""
def draw(self):
raise NotImplementedError
class Circle2D:
def draw(self):
return "Drawing a circle: O"
class Square2D:
def draw(self):
return "Drawing a square: □"
class Triangle2D:
def draw(self):
return "Drawing a triangle: △"
class Text2D:
def __init__(self, text):
self.text = text
def draw(self):
return f"Drawing text: '{self.text}'"
def render_canvas(drawables):
"""Render anything that has a draw() method."""
print("Canvas:")
for drawable in drawables:
print(f" {drawable.draw()}")
# All these can be rendered
elements = [Circle2D(), Square2D(), Triangle2D(), Text2D("Hello")]
render_canvas(elements)
# -----------------------------------------------------------------------------
# 8. Polymorphism with Abstract Base Classes
# -----------------------------------------------------------------------------
print("\n--- ABC Polymorphism ---")
from abc import ABC, abstractmethod
class DataExporter(ABC):
"""Abstract base class for data exporters."""
@abstractmethod
def export(self, data):
"""Export data - must be implemented."""
pass
def validate(self, data):
"""Common validation (can be overridden)."""
if not data:
raise ValueError("No data to export")
return True
class JSONExporter(DataExporter):
def export(self, data):
import json
self.validate(data)
return json.dumps(data, indent=2)
class CSVExporter(DataExporter):
def export(self, data):
self.validate(data)
if not data:
return ""
headers = ",".join(data[0].keys())
rows = [",".join(str(v) for v in row.values()) for row in data]
return headers + "\n" + "\n".join(rows)
class XMLExporter(DataExporter):
def export(self, data):
self.validate(data)
xml = "<root>\n"
for item in data:
xml += " <item>\n"
for key, value in item.items():
xml += f" <{key}>{value}</{key}>\n"
xml += " </item>\n"
xml += "</root>"
return xml
# Same data, different formats
sample_data = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]
exporters = [JSONExporter(), CSVExporter(), XMLExporter()]
print("Exporting same data in different formats:")
for exporter in exporters:
print(f"\n{type(exporter).__name__}:")
print(exporter.export(sample_data))
# -----------------------------------------------------------------------------
# 9. Method Dispatch Based on Type
# -----------------------------------------------------------------------------
print("\n--- Type-Based Method Dispatch ---")
from functools import singledispatch
@singledispatch
def process(value):
"""Default processing for unknown types."""
return f"Don't know how to process {type(value).__name__}"
@process.register(int)
def _(value):
"""Process integers."""
return f"Integer: {value * 2}"
@process.register(str)
def _(value):
"""Process strings."""
return f"String: {value.upper()}"
@process.register(list)
def _(value):
"""Process lists."""
return f"List with {len(value)} items"
# Same function name, different behavior based on type
print("Single dispatch polymorphism:")
print(f" process(42): {process(42)}")
print(f" process('hello'): {process('hello')}")
print(f" process([1,2,3]): {process([1,2,3])}")
print(f" process(3.14): {process(3.14)}")
# -----------------------------------------------------------------------------
# 10. Practical Example: Notification System
# -----------------------------------------------------------------------------
print("\n--- Practical Example: Notification System ---")
class NotificationService(ABC):
"""Abstract base for notification services."""
@abstractmethod
def send(self, recipient, message):
"""Send a notification."""
pass
@abstractmethod
def get_status(self):
"""Get service status."""
pass
class EmailNotification(NotificationService):
def __init__(self, smtp_server="mail.example.com"):
self.smtp_server = smtp_server
def send(self, recipient, message):
return f"📧 Email sent to {recipient}: '{message}'"
def get_status(self):
return f"Email service connected to {self.smtp_server}"
class SMSNotification(NotificationService):
def __init__(self, provider="TwilioMock"):
self.provider = provider
def send(self, recipient, message):
return f"📱 SMS sent to {recipient}: '{message[:50]}...'"
def get_status(self):
return f"SMS service using {self.provider}"
class PushNotification(NotificationService):
def __init__(self, app_name="MyApp"):
self.app_name = app_name
def send(self, recipient, message):
return f"🔔 Push notification to {recipient}: '{message}'"
def get_status(self):
return f"Push service for {self.app_name}"
class SlackNotification(NotificationService):
def __init__(self, workspace="MyWorkspace"):
self.workspace = workspace
def send(self, recipient, message):
return f"💬 Slack message to #{recipient}: '{message}'"
def get_status(self):
return f"Slack connected to {self.workspace}"
# Polymorphic notification manager
class NotificationManager:
"""Manages multiple notification services."""
def __init__(self):
self.services = []
def add_service(self, service):
self.services.append(service)
def send_all(self, recipient, message):
"""Send via all services."""
results = []
for service in self.services:
results.append(service.send(recipient, message))
return results
def status(self):
"""Get status of all services."""
return [service.get_status() for service in self.services]
# Create manager and add services
manager = NotificationManager()
manager.add_service(EmailNotification())
manager.add_service(SMSNotification())
manager.add_service(PushNotification())
manager.add_service(SlackNotification())
print("Service status:")
for status in manager.status():
print(f"{status}")
print("\nSending notification via all channels:")
for result in manager.send_all("user123", "Your order has been shipped!"):
print(f" {result}")

View File

@ -0,0 +1,304 @@
"""
================================================================================
File: 01_list_comprehensions.py
Topic: List Comprehensions and Other Comprehensions
================================================================================
This file demonstrates comprehensions in Python - a concise and powerful way
to create lists, dictionaries, sets, and generators. Comprehensions are more
readable and often faster than traditional loops.
Key Concepts:
- List comprehensions
- Dictionary comprehensions
- Set comprehensions
- Generator expressions
- Nested comprehensions
- When to use comprehensions
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic List Comprehension
# -----------------------------------------------------------------------------
# [expression for item in iterable]
print("--- Basic List Comprehension ---")
# Traditional way
squares_loop = []
for x in range(1, 6):
squares_loop.append(x ** 2)
print(f"Loop method: {squares_loop}")
# List comprehension way
squares_comp = [x ** 2 for x in range(1, 6)]
print(f"Comprehension: {squares_comp}")
# More examples
numbers = [1, 2, 3, 4, 5]
doubled = [n * 2 for n in numbers]
strings = [str(n) for n in numbers]
print(f"\nOriginal: {numbers}")
print(f"Doubled: {doubled}")
print(f"As strings: {strings}")
# -----------------------------------------------------------------------------
# 2. List Comprehension with Condition
# -----------------------------------------------------------------------------
# [expression for item in iterable if condition]
print("\n--- Comprehension with Condition ---")
# Get only even numbers
numbers = range(1, 21)
evens = [n for n in numbers if n % 2 == 0]
print(f"Even numbers: {evens}")
# Filter and transform
words = ["hello", "world", "python", "AI", "ml"]
long_upper = [w.upper() for w in words if len(w) > 3]
print(f"Long words (uppercase): {long_upper}")
# Multiple conditions (AND)
numbers = range(1, 51)
divisible_by_3_and_5 = [n for n in numbers if n % 3 == 0 and n % 5 == 0]
print(f"Divisible by 3 and 5: {divisible_by_3_and_5}")
# -----------------------------------------------------------------------------
# 3. Conditional Expression in Comprehension
# -----------------------------------------------------------------------------
# [expr1 if condition else expr2 for item in iterable]
print("\n--- Conditional Expression ---")
# Replace negatives with zero
numbers = [-3, -1, 0, 2, 5, -4, 8]
non_negative = [n if n >= 0 else 0 for n in numbers]
print(f"Original: {numbers}")
print(f"Non-negative: {non_negative}")
# Categorize numbers
categorized = ["even" if n % 2 == 0 else "odd" for n in range(1, 6)]
print(f"Categories: {categorized}")
# Pass/fail based on score
scores = [85, 42, 91, 78, 55]
results = ["Pass" if s >= 60 else "Fail" for s in scores]
print(f"Scores: {scores}")
print(f"Results: {results}")
# -----------------------------------------------------------------------------
# 4. Nested Loops in Comprehension
# -----------------------------------------------------------------------------
print("\n--- Nested Loops ---")
# Flatten a 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(f"Matrix: {matrix}")
print(f"Flattened: {flat}")
# All combinations
colors = ["red", "green"]
sizes = ["S", "M", "L"]
combinations = [(color, size) for color in colors for size in sizes]
print(f"\nCombinations: {combinations}")
# Multiplication table
table = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print("\nMultiplication table:")
for row in table:
print(f" {row}")
# -----------------------------------------------------------------------------
# 5. Dictionary Comprehensions
# -----------------------------------------------------------------------------
# {key: value for item in iterable}
print("\n--- Dictionary Comprehensions ---")
# Square numbers dictionary
squares_dict = {x: x ** 2 for x in range(1, 6)}
print(f"Squares dict: {squares_dict}")
# From two lists
keys = ["name", "age", "city"]
values = ["Alice", 25, "NYC"]
person = {k: v for k, v in zip(keys, values)}
print(f"Person dict: {person}")
# Swap keys and values
original = {"a": 1, "b": 2, "c": 3}
swapped = {v: k for k, v in original.items()}
print(f"Original: {original}")
print(f"Swapped: {swapped}")
# Filter dictionary
scores = {"Alice": 85, "Bob": 42, "Charlie": 91, "Dave": 55}
passing = {name: score for name, score in scores.items() if score >= 60}
print(f"\nPasssing students: {passing}")
# Transform dictionary
prices = {"apple": 1.5, "banana": 0.75, "cherry": 2.0}
taxed = {item: price * 1.1 for item, price in prices.items()}
print(f"With tax: {taxed}")
# -----------------------------------------------------------------------------
# 6. Set Comprehensions
# -----------------------------------------------------------------------------
# {expression for item in iterable}
print("\n--- Set Comprehensions ---")
# Unique squares
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique_squares = {x ** 2 for x in numbers}
print(f"Numbers: {numbers}")
print(f"Unique squares: {unique_squares}")
# Unique first letters
words = ["apple", "banana", "apricot", "blueberry", "cherry"]
first_letters = {w[0] for w in words}
print(f"First letters: {first_letters}")
# Get unique word lengths
sentences = "the quick brown fox jumps over the lazy dog"
word_lengths = {len(word) for word in sentences.split()}
print(f"Unique word lengths: {word_lengths}")
# -----------------------------------------------------------------------------
# 7. Generator Expressions
# -----------------------------------------------------------------------------
# (expression for item in iterable) - lazy evaluation!
print("\n--- Generator Expressions ---")
# Generator expression (note the parentheses)
squares_gen = (x ** 2 for x in range(1, 6))
print(f"Generator object: {squares_gen}")
print(f"As list: {list(squares_gen)}")
# Memory efficient for large data
# List: creates all values immediately
# Generator: creates values on demand
# Sum of squares (generator is more memory efficient)
total = sum(x ** 2 for x in range(1, 1001))
print(f"Sum of squares 1-1000: {total}")
# Check if any/all
numbers = [1, 3, 5, 7, 9]
any_even = any(n % 2 == 0 for n in numbers)
all_positive = all(n > 0 for n in numbers)
print(f"\nNumbers: {numbers}")
print(f"Any even? {any_even}")
print(f"All positive? {all_positive}")
# Find first match
names = ["Alice", "Bob", "Charlie", "Diana"]
first_long = next((name for name in names if len(name) > 5), None)
print(f"First name > 5 chars: {first_long}")
# -----------------------------------------------------------------------------
# 8. Nested Comprehensions
# -----------------------------------------------------------------------------
print("\n--- Nested Comprehensions ---")
# Create a matrix
rows, cols = 3, 4
matrix = [[0 for _ in range(cols)] for _ in range(rows)]
print(f"Zero matrix {rows}x{cols}: {matrix}")
# Identity matrix
identity = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
print("\nIdentity matrix:")
for row in identity:
print(f" {row}")
# Transpose matrix
original = [[1, 2, 3], [4, 5, 6]]
transposed = [[row[i] for row in original] for i in range(len(original[0]))]
print(f"\nOriginal: {original}")
print(f"Transposed: {transposed}")
# -----------------------------------------------------------------------------
# 9. When NOT to Use Comprehensions
# -----------------------------------------------------------------------------
print("\n--- When NOT to Use Comprehensions ---")
# 1. Complex logic - use regular loop
# BAD:
# result = [func1(x) if x > 0 else func2(x) if x < 0 else func3(x) for x in data if valid(x)]
# GOOD: Use regular loop for complex logic
def process_numbers(data):
result = []
for x in data:
if not x: # Skip None or 0
continue
if x > 0:
result.append(x ** 2)
else:
result.append(abs(x))
return result
# 2. Side effects - use regular loop
# BAD: [print(x) for x in items] # Creates unnecessary list
# GOOD:
# for x in items:
# print(x)
# 3. Very long comprehensions that require wrapping
# Consider regular loop for readability
print("Use regular loops when:")
print(" - Logic is complex")
print(" - There are side effects (print, modify, etc.)")
print(" - Comprehension becomes too long to read")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# 1. Parse CSV-like data
csv_data = "name,age,city\nAlice,25,NYC\nBob,30,LA\nCharlie,35,Chicago"
rows = [line.split(",") for line in csv_data.split("\n")]
print(f"Parsed CSV: {rows}")
# 2. Filter and transform objects
users = [
{"name": "Alice", "age": 25, "active": True},
{"name": "Bob", "age": 17, "active": True},
{"name": "Charlie", "age": 30, "active": False},
{"name": "Diana", "age": 22, "active": True}
]
active_adults = [
user["name"]
for user in users
if user["active"] and user["age"] >= 18
]
print(f"\nActive adults: {active_adults}")
# 3. Word frequency (dict comprehension)
text = "the quick brown fox jumps over the lazy dog"
words = text.split()
frequency = {word: words.count(word) for word in set(words)}
print(f"\nWord frequency: {frequency}")
# 4. File path manipulation
files = ["report.pdf", "data.csv", "image.png", "document.pdf", "log.txt"]
pdf_files = [f for f in files if f.endswith(".pdf")]
print(f"\nPDF files: {pdf_files}")
# 5. Coordinate pairs
coords = [(x, y) for x in range(3) for y in range(3) if x != y]
print(f"\nCoordinate pairs (x != y): {coords}")

View File

@ -0,0 +1,363 @@
"""
================================================================================
File: 02_generators.py
Topic: Generators and Iterators in Python
================================================================================
This file demonstrates generators and iterators in Python. Generators are
special functions that can pause and resume execution, yielding values one
at a time. They're memory-efficient for processing large data.
Key Concepts:
- Iterator protocol (__iter__, __next__)
- Generator functions (yield)
- Generator expressions
- yield from
- Practical applications
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Understanding Iterators
# -----------------------------------------------------------------------------
# Iterators implement __iter__ and __next__
print("--- Understanding Iterators ---")
# Lists are iterable (have __iter__)
numbers = [1, 2, 3]
iterator = iter(numbers) # Get iterator
print(f"First: {next(iterator)}")
print(f"Second: {next(iterator)}")
print(f"Third: {next(iterator)}")
# next(iterator) # Would raise StopIteration
# Custom iterator class
class CountDown:
"""A countdown iterator."""
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print("\nCountdown iterator:")
for num in CountDown(5):
print(f" {num}")
# -----------------------------------------------------------------------------
# 2. Generator Functions
# -----------------------------------------------------------------------------
# Use yield to create generators
print("\n--- Generator Functions ---")
def countdown(n):
"""Generator function for countdown."""
while n > 0:
yield n # Pause here and return value
n -= 1 # Resume from here on next()
# Using the generator
print("Generator countdown:")
for num in countdown(5):
print(f" {num}")
# Generator is an iterator
gen = countdown(3)
print(f"\nGenerator object: {gen}")
print(f"next(): {next(gen)}")
print(f"next(): {next(gen)}")
print(f"next(): {next(gen)}")
# -----------------------------------------------------------------------------
# 3. Generators vs Lists - Memory Efficiency
# -----------------------------------------------------------------------------
print("\n--- Memory Efficiency ---")
import sys
# List - all values in memory at once
list_nums = [x ** 2 for x in range(1000)]
list_size = sys.getsizeof(list_nums)
# Generator - values created on demand
gen_nums = (x ** 2 for x in range(1000))
gen_size = sys.getsizeof(gen_nums)
print(f"List of 1000 squares: {list_size:,} bytes")
print(f"Generator for 1000 squares: {gen_size} bytes")
print(f"Memory saved: {list_size - gen_size:,} bytes")
# For very large sequences, generators are essential
# This would crash with a list: sum(range(10**12))
# But works with generator: sum(range(10**6)) # Uses constant memory
# -----------------------------------------------------------------------------
# 4. Infinite Generators
# -----------------------------------------------------------------------------
print("\n--- Infinite Generators ---")
def infinite_counter(start=0):
"""Generate numbers infinitely."""
n = start
while True:
yield n
n += 1
def fibonacci():
"""Generate Fibonacci numbers infinitely."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Use islice to take limited values from infinite generator
from itertools import islice
print("First 10 counter values:")
counter = infinite_counter(10)
first_10 = list(islice(counter, 10))
print(f" {first_10}")
print("\nFirst 10 Fibonacci numbers:")
fib = fibonacci()
fib_10 = list(islice(fib, 10))
print(f" {fib_10}")
# -----------------------------------------------------------------------------
# 5. Generator Methods
# -----------------------------------------------------------------------------
print("\n--- Generator Methods ---")
def interactive_generator():
"""Generator that can receive values."""
print(" Generator started")
# yield can receive values via send()
received = yield "First yield"
print(f" Received: {received}")
received = yield "Second yield"
print(f" Received: {received}")
yield "Final yield"
gen = interactive_generator()
print(f"next(): {next(gen)}") # Start and get first yield
print(f"send('Hello'): {gen.send('Hello')}") # Send value, get second yield
print(f"send('World'): {gen.send('World')}") # Send value, get final yield
# -----------------------------------------------------------------------------
# 6. yield from - Delegating Generators
# -----------------------------------------------------------------------------
print("\n--- yield from ---")
def sub_generator():
"""A simple sub-generator."""
yield 1
yield 2
yield 3
# Without yield from
def main_generator_old():
for item in sub_generator():
yield item
yield 4
yield 5
# With yield from (cleaner)
def main_generator():
yield from sub_generator()
yield 4
yield 5
print("Using yield from:")
result = list(main_generator())
print(f" {result}")
# Flatten nested structure with yield from
def flatten(nested):
"""Flatten arbitrarily nested lists."""
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
print(f"\nFlattening: {nested}")
print(f"Result: {list(flatten(nested))}")
# -----------------------------------------------------------------------------
# 7. Generator Pipelines
# -----------------------------------------------------------------------------
print("\n--- Generator Pipelines ---")
# Process data through multiple generators (like Unix pipes)
def read_data():
"""Simulate reading data."""
data = [" John,25,NYC ", " Jane,30,LA ", " Bob,35,Chicago ", ""]
for line in data:
yield line
def strip_lines(lines):
"""Remove whitespace from lines."""
for line in lines:
yield line.strip()
def filter_empty(lines):
"""Filter out empty lines."""
for line in lines:
if line:
yield line
def parse_csv(lines):
"""Parse CSV lines into dictionaries."""
for line in lines:
name, age, city = line.split(",")
yield {"name": name, "age": int(age), "city": city}
# Chain generators together
pipeline = parse_csv(filter_empty(strip_lines(read_data())))
print("Pipeline processing:")
for record in pipeline:
print(f" {record}")
# -----------------------------------------------------------------------------
# 8. Context Manager with Generator
# -----------------------------------------------------------------------------
print("\n--- Generator as Context Manager ---")
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
"""A context manager using generator."""
print(f" Acquiring: {name}")
try:
yield name # Resource is available here
finally:
print(f" Releasing: {name}")
# Using the context manager
print("Using managed resource:")
with managed_resource("Database Connection") as conn:
print(f" Using: {conn}")
# -----------------------------------------------------------------------------
# 9. Built-in Generator Functions
# -----------------------------------------------------------------------------
print("\n--- Built-in Generator Functions ---")
# range() is a generator-like object
print(f"range(5): {list(range(5))}")
# enumerate() yields (index, value)
fruits = ["apple", "banana", "cherry"]
print(f"\nenumerate(): {list(enumerate(fruits))}")
# zip() yields tuples from multiple iterables
names = ["Alice", "Bob"]
ages = [25, 30]
print(f"zip(): {list(zip(names, ages))}")
# map() yields transformed values
print(f"map(str.upper): {list(map(str.upper, fruits))}")
# filter() yields filtered values
numbers = [1, 2, 3, 4, 5, 6]
print(f"filter(even): {list(filter(lambda x: x % 2 == 0, numbers))}")
# itertools has many useful generators
from itertools import chain, cycle, repeat, takewhile
# chain - combine iterables
print(f"\nchain([1,2], [3,4]): {list(chain([1,2], [3,4]))}")
# takewhile - yield while condition is true
nums = [2, 4, 6, 7, 8, 10]
print(f"takewhile(even): {list(takewhile(lambda x: x % 2 == 0, nums))}")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# 1. Reading large files line by line
def read_large_file(filepath):
"""Read file lazily, one line at a time."""
# In real code: yield from open(filepath)
# Here we simulate:
lines = ["Line 1", "Line 2", "Line 3"]
for line in lines:
yield line
print("Reading 'file' lazily:")
for line in read_large_file("data.txt"):
print(f" Processing: {line}")
# 2. Paginated API results
def fetch_paginated_data(total_pages=3):
"""Simulate fetching paginated API data."""
for page in range(1, total_pages + 1):
print(f" Fetching page {page}...")
yield [f"item_{page}_{i}" for i in range(3)]
print("\nPaginated data:")
for items in fetch_paginated_data():
for item in items:
print(f" {item}")
# 3. Sliding window
def sliding_window(iterable, size):
"""Generate sliding windows over data."""
from collections import deque
window = deque(maxlen=size)
for item in iterable:
window.append(item)
if len(window) == size:
yield tuple(window)
data = [1, 2, 3, 4, 5, 6]
print(f"\nSliding window (size 3) over {data}:")
for window in sliding_window(data, 3):
print(f" {window}")
# 4. Batch processing
def batch(iterable, size):
"""Yield items in batches."""
batch = []
for item in iterable:
batch.append(item)
if len(batch) == size:
yield batch
batch = []
if batch:
yield batch
items = list(range(1, 11))
print(f"\nBatching {items} into groups of 3:")
for b in batch(items, 3):
print(f" {b}")

View File

@ -0,0 +1,421 @@
"""
================================================================================
File: 03_decorators.py
Topic: Decorators in Python
================================================================================
This file demonstrates decorators in Python. Decorators are a powerful feature
that allows you to modify or enhance functions without changing their code.
They use the @ syntax and are widely used for logging, authentication, caching,
and more.
Key Concepts:
- Function as first-class objects
- Simple decorators
- Decorators with arguments
- Preserving function metadata
- Class decorators
- Built-in decorators
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Functions as First-Class Objects
# -----------------------------------------------------------------------------
# Functions can be assigned, passed, and returned
print("--- Functions as First-Class Objects ---")
def greet(name):
return f"Hello, {name}!"
# Assign to variable
say_hello = greet
print(say_hello("World"))
# Pass as argument
def apply_func(func, value):
return func(value)
print(apply_func(greet, "Python"))
# Return from function
def get_greeter():
def inner_greet(name):
return f"Hi, {name}!"
return inner_greet
greeter = get_greeter()
print(greeter("Alice"))
# -----------------------------------------------------------------------------
# 2. Simple Decorator
# -----------------------------------------------------------------------------
# A decorator wraps a function to add behavior
print("\n--- Simple Decorator ---")
def my_decorator(func):
"""A simple decorator."""
def wrapper(*args, **kwargs):
print(" Before function call")
result = func(*args, **kwargs)
print(" After function call")
return result
return wrapper
# Applying decorator manually
def say_hello_v1(name):
print(f" Hello, {name}!")
decorated = my_decorator(say_hello_v1)
decorated("World")
# Using @ syntax (syntactic sugar)
@my_decorator
def say_hello_v2(name):
print(f" Hello, {name}!")
print("\nWith @ syntax:")
say_hello_v2("Python")
# -----------------------------------------------------------------------------
# 3. Preserving Function Metadata
# -----------------------------------------------------------------------------
# Use functools.wraps to preserve original function info
print("\n--- Preserving Metadata ---")
from functools import wraps
def better_decorator(func):
"""Decorator that preserves function metadata."""
@wraps(func) # Preserve original function's metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@better_decorator
def example_function():
"""This is the docstring."""
pass
print(f"Function name: {example_function.__name__}")
print(f"Function docstring: {example_function.__doc__}")
# Without @wraps, these would show 'wrapper' info instead
# -----------------------------------------------------------------------------
# 4. Practical Decorators
# -----------------------------------------------------------------------------
print("\n--- Practical Decorators ---")
import time
# Timing decorator
def timer(func):
"""Measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f" {func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
"""A deliberately slow function."""
time.sleep(0.1)
return "Done"
result = slow_function()
# Logging decorator
def logger(func):
"""Log function calls."""
@wraps(func)
def wrapper(*args, **kwargs):
args_str = ", ".join(map(repr, args))
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
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
@logger
def add(a, b):
return a + b
print("\nLogger example:")
add(3, 5)
# -----------------------------------------------------------------------------
# 5. Decorators with Arguments
# -----------------------------------------------------------------------------
# Create configurable decorators with an extra layer
print("\n--- Decorators with Arguments ---")
def repeat(times):
"""Decorator to repeat function calls."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(times=3)
def greet_user(name):
print(f" Hello, {name}!")
return f"Greeted {name}"
print("Repeat decorator:")
results = greet_user("Alice")
print(f"Results: {results}")
# Retry decorator with custom attempts
def retry(max_attempts=3, exceptions=(Exception,)):
"""Retry function on failure."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f" Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
return wrapper
return decorator
attempt_count = 0
@retry(max_attempts=3)
def unstable_function():
global attempt_count
attempt_count += 1
if attempt_count < 3:
raise ValueError("Not ready yet!")
return "Success!"
print("\nRetry decorator:")
try:
result = unstable_function()
print(f" Final result: {result}")
except ValueError:
print(" All attempts failed")
# -----------------------------------------------------------------------------
# 6. Multiple Decorators
# -----------------------------------------------------------------------------
# Decorators stack from bottom to top
print("\n--- Multiple Decorators ---")
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet_html(name):
return f"Hello, {name}"
# Applied bottom-up: italic first, then bold
# Same as: bold(italic(greet_html))
print(f"Stacked decorators: {greet_html('World')}")
# -----------------------------------------------------------------------------
# 7. Class-Based Decorators
# -----------------------------------------------------------------------------
# Use a class as a decorator
print("\n--- Class-Based Decorators ---")
class CallCounter:
"""Decorator class to count function calls."""
def __init__(self, func):
self.func = func
self.count = 0
# Preserve function attributes
self.__name__ = func.__name__
self.__doc__ = func.__doc__
def __call__(self, *args, **kwargs):
self.count += 1
print(f" {self.func.__name__} has been called {self.count} time(s)")
return self.func(*args, **kwargs)
@CallCounter
def hello():
"""Say hello."""
print(" Hello!")
hello()
hello()
hello()
print(f"Total calls: {hello.count}")
# -----------------------------------------------------------------------------
# 8. Built-in Decorators
# -----------------------------------------------------------------------------
print("\n--- Built-in Decorators ---")
# @property - create getter/setter
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
import math
return math.pi * self._radius ** 2
circle = Circle(5)
print(f"Circle radius: {circle.radius}")
print(f"Circle area: {circle.area:.2f}")
# @staticmethod - method without self
class Math:
@staticmethod
def add(a, b):
return a + b
print(f"\n@staticmethod: Math.add(3, 4) = {Math.add(3, 4)}")
# @classmethod - method with cls instead of self
class Counter:
count = 0
def __init__(self):
Counter.count += 1
@classmethod
def get_count(cls):
return cls.count
c1, c2, c3 = Counter(), Counter(), Counter()
print(f"@classmethod: Counter.get_count() = {Counter.get_count()}")
# @functools.lru_cache - memoization
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(f"\n@lru_cache: fibonacci(30) = {fibonacci(30)}")
print(f"Cache info: {fibonacci.cache_info()}")
# -----------------------------------------------------------------------------
# 9. Decorator for Methods
# -----------------------------------------------------------------------------
print("\n--- Decorating Methods ---")
def debug_method(func):
"""Debug decorator for class methods."""
@wraps(func)
def wrapper(self, *args, **kwargs):
class_name = self.__class__.__name__
print(f" {class_name}.{func.__name__} called")
return func(self, *args, **kwargs)
return wrapper
class Calculator:
@debug_method
def add(self, a, b):
return a + b
@debug_method
def multiply(self, a, b):
return a * b
calc = Calculator()
calc.add(3, 5)
calc.multiply(4, 6)
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# 1. Authorization decorator
def require_auth(func):
"""Check if user is authenticated."""
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("authenticated", False):
raise PermissionError("Authentication required")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_secret_data(user):
return f"Secret data for {user['name']}"
print("Authorization decorator:")
try:
result = get_secret_data({"name": "Guest", "authenticated": False})
except PermissionError as e:
print(f" Denied: {e}")
result = get_secret_data({"name": "Admin", "authenticated": True})
print(f" Allowed: {result}")
# 2. Validation decorator
def validate_positive(func):
"""Ensure all numeric arguments are positive."""
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Negative value not allowed: {arg}")
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
return width * height
print("\nValidation decorator:")
try:
calculate_area(-5, 10)
except ValueError as e:
print(f" Validation error: {e}")
print(f" Valid call: {calculate_area(5, 10)}")

View File

@ -0,0 +1,382 @@
"""
================================================================================
File: 04_context_managers.py
Topic: Context Managers in Python
================================================================================
This file demonstrates context managers in Python. Context managers are objects
that define setup and cleanup actions, typically used with the 'with' statement.
They ensure resources are properly managed even when errors occur.
Key Concepts:
- The 'with' statement
- __enter__ and __exit__ methods
- contextlib module
- File handling
- Resource management patterns
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. The 'with' Statement
# -----------------------------------------------------------------------------
# Ensures cleanup happens automatically
print("--- The 'with' Statement ---")
# Most common use: file handling
# Without 'with' (error-prone)
"""
file = open('example.txt', 'w')
try:
file.write('Hello')
finally:
file.close() # Must remember to close!
"""
# With 'with' (automatic cleanup)
"""
with open('example.txt', 'w') as file:
file.write('Hello')
# File is automatically closed here
"""
print("The 'with' statement:")
print(" - Automatically handles setup and cleanup")
print(" - Ensures cleanup even if errors occur")
print(" - Cleaner, more readable code")
# -----------------------------------------------------------------------------
# 2. Creating a Context Manager Class
# -----------------------------------------------------------------------------
# Implement __enter__ and __exit__
print("\n--- Context Manager Class ---")
class ManagedFile:
"""A custom context manager for file handling."""
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""Called when entering 'with' block."""
print(f" Opening file: {self.filename}")
# In real code: self.file = open(self.filename, self.mode)
self.file = f"<FileObject: {self.filename}>"
return self.file # This is assigned to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting 'with' block."""
print(f" Closing file: {self.filename}")
# In real code: self.file.close()
# exc_type, exc_val, exc_tb contain exception info if an error occurred
if exc_type is not None:
print(f" Exception occurred: {exc_type.__name__}: {exc_val}")
# Return True to suppress the exception, False to propagate
return False
# Using the context manager
print("Normal usage:")
with ManagedFile("data.txt", "w") as f:
print(f" Working with: {f}")
print("\nWith exception:")
try:
with ManagedFile("data.txt") as f:
print(f" Working with: {f}")
raise ValueError("Something went wrong!")
except ValueError:
print(" Exception was propagated")
# -----------------------------------------------------------------------------
# 3. Context Manager with Exception Suppression
# -----------------------------------------------------------------------------
print("\n--- Suppressing Exceptions ---")
class SuppressErrors:
"""Context manager that suppresses specified exceptions."""
def __init__(self, *exceptions):
self.exceptions = exceptions
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None and issubclass(exc_type, self.exceptions):
print(f" Suppressed: {exc_type.__name__}: {exc_val}")
return True # Suppress the exception
return False # Don't suppress
# Using the suppressor
print("Suppressing ValueError:")
with SuppressErrors(ValueError, TypeError):
print(" Before error")
raise ValueError("This will be suppressed")
print(" After error") # Never reached
print(" Code continues after 'with' block")
# -----------------------------------------------------------------------------
# 4. Using contextlib
# -----------------------------------------------------------------------------
# Create context managers without classes
print("\n--- Using contextlib ---")
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
"""A context manager using @contextmanager decorator."""
print(f" Acquiring: {name}")
try:
yield name # Control is passed to 'with' block here
finally:
print(f" Releasing: {name}")
print("@contextmanager decorator:")
with managed_resource("Database") as resource:
print(f" Using: {resource}")
# Timer context manager
import time
@contextmanager
def timer(description="Operation"):
"""Time the enclosed code block."""
start = time.time()
try:
yield
finally:
elapsed = time.time() - start
print(f" {description} took {elapsed:.4f} seconds")
print("\nTimer context manager:")
with timer("Sleep operation"):
time.sleep(0.1)
# -----------------------------------------------------------------------------
# 5. Multiple Context Managers
# -----------------------------------------------------------------------------
print("\n--- Multiple Context Managers ---")
@contextmanager
def open_mock_file(name):
"""Mock file context manager."""
print(f" Opening: {name}")
yield f"<{name}>"
print(f" Closing: {name}")
# Multiple context managers in one 'with'
print("Multiple context managers:")
with open_mock_file("input.txt") as infile, \
open_mock_file("output.txt") as outfile:
print(f" Reading from: {infile}")
print(f" Writing to: {outfile}")
# Python 3.10+ allows parenthesized context managers
"""
with (
open_mock_file("input.txt") as infile,
open_mock_file("output.txt") as outfile
):
...
"""
# -----------------------------------------------------------------------------
# 6. Practical Context Managers
# -----------------------------------------------------------------------------
print("\n--- Practical Context Managers ---")
# 1. Database connection (mock)
@contextmanager
def database_connection(host):
"""Mock database connection."""
print(f" Connecting to {host}...")
connection = {"host": host, "connected": True}
try:
yield connection
finally:
connection["connected"] = False
print(f" Disconnected from {host}")
print("Database connection:")
with database_connection("localhost") as db:
print(f" Connected: {db['connected']}")
# Perform database operations
# 2. Temporary working directory
import os
from contextlib import contextmanager
@contextmanager
def temp_directory_context():
"""Change to temp directory and restore."""
original_dir = os.getcwd()
temp_dir = os.path.dirname(original_dir) if original_dir else original_dir
try:
# In real code: os.chdir(temp_dir)
print(f" Changed to: {temp_dir}")
yield temp_dir
finally:
# os.chdir(original_dir)
print(f" Restored to: {original_dir}")
print("\nDirectory change:")
with temp_directory_context():
print(" Working in temp directory")
# 3. Lock context manager
@contextmanager
def locked(lock_name="default"):
"""Mock lock context manager."""
print(f" Acquiring lock: {lock_name}")
try:
yield
finally:
print(f" Releasing lock: {lock_name}")
print("\nLocking:")
with locked("resource_lock"):
print(" Critical section")
# -----------------------------------------------------------------------------
# 7. contextlib Utilities
# -----------------------------------------------------------------------------
print("\n--- contextlib Utilities ---")
from contextlib import closing, suppress
# closing() - for objects with close() but no __exit__
class Connection:
def close(self):
print(" Connection closed")
print("using closing():")
with closing(Connection()) as conn:
print(" Using connection")
# suppress() - ignore specified exceptions
print("\nsuppress() example:")
from contextlib import suppress
# Instead of try/except for simple cases
with suppress(FileNotFoundError, KeyError):
# This would normally raise an error
print(" Attempting risky operations...")
# raise FileNotFoundError("No such file")
print(" Continued after suppress")
# nullcontext() - do-nothing context manager (Python 3.7+)
from contextlib import nullcontext
def process_data(data, lock=None):
"""Process with optional lock."""
with lock if lock else nullcontext():
print(f" Processing: {data}")
print("\nnullcontext() example:")
process_data("data1") # No lock
process_data("data2", locked("my_lock")) # With lock
# -----------------------------------------------------------------------------
# 8. Async Context Managers
# -----------------------------------------------------------------------------
print("\n--- Async Context Managers ---")
# For async code, use __aenter__ and __aexit__
async_example = '''
class AsyncResource:
async def __aenter__(self):
print("Acquiring async resource...")
await asyncio.sleep(0.1)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Releasing async resource...")
await asyncio.sleep(0.1)
return False
# Usage:
async with AsyncResource() as resource:
print("Using resource")
'''
print("Async context managers use __aenter__ and __aexit__")
print("Use 'async with' statement")
# -----------------------------------------------------------------------------
# 9. Reentrant and Reusable Context Managers
# -----------------------------------------------------------------------------
print("\n--- Reentrant Context Managers ---")
# A context manager that can be used multiple times
class ReusableContext:
"""Context manager that can be reused."""
def __init__(self, name):
self.name = name
self.uses = 0
def __enter__(self):
self.uses += 1
print(f" Entering {self.name} (use #{self.uses})")
return self
def __exit__(self, *args):
print(f" Exiting {self.name}")
return False
ctx = ReusableContext("MyContext")
print("Reusing context manager:")
with ctx:
print(" First use")
with ctx:
print(" Second use")
print(f"Total uses: {ctx.uses}")
# -----------------------------------------------------------------------------
# 10. Real-World Pattern: Configuration Override
# -----------------------------------------------------------------------------
print("\n--- Real-World: Config Override ---")
class Config:
"""Application configuration."""
settings = {"debug": False, "log_level": "INFO"}
@classmethod
@contextmanager
def override(cls, **overrides):
"""Temporarily override configuration."""
original = cls.settings.copy()
try:
cls.settings.update(overrides)
print(f" Config overridden: {overrides}")
yield cls.settings
finally:
cls.settings = original
print(" Config restored")
print(f"Original config: {Config.settings}")
with Config.override(debug=True, log_level="DEBUG") as settings:
print(f" Inside 'with': {settings}")
print(f"After 'with': {Config.settings}")

View File

@ -0,0 +1,334 @@
"""
================================================================================
File: 01_pep8.py
Topic: PEP 8 - Python Style Guide
================================================================================
This file demonstrates PEP 8, the official Python style guide. Following PEP 8
makes your code more readable, consistent, and professional. These are
conventions, not strict rules, but following them is highly recommended.
Key Concepts:
- Indentation and whitespace
- Naming conventions
- Line length and wrapping
- Imports organization
- Comments and docstrings
Reference: https://peps.python.org/pep-0008/
================================================================================
"""
# =============================================================================
# 1. INDENTATION
# =============================================================================
# Use 4 spaces per indentation level. Never mix tabs and spaces.
# GOOD
def function_with_proper_indentation():
if True:
for i in range(10):
print(i)
# Aligned with opening delimiter
foo = long_function_name(var_one, var_two,
var_three, var_four)
# Hanging indent (add a level)
def long_function_name(
var_one, var_two, var_three,
var_four):
print(var_one)
# =============================================================================
# 2. LINE LENGTH
# =============================================================================
# Limit lines to 79 characters (72 for docstrings/comments)
# GOOD - Use implicit line continuation
total = (first_variable
+ second_variable
+ third_variable)
# GOOD - Use backslash when necessary
with open('/very/long/path/to/file.txt') as file_one, \
open('/another/long/path/to/file.txt') as file_two:
pass
# =============================================================================
# 3. BLANK LINES
# =============================================================================
# - 2 blank lines around top-level functions and classes
# - 1 blank line between methods in a class
class FirstClass:
"""First class."""
pass
class SecondClass:
"""Second class."""
def method_one(self):
"""First method."""
pass
def method_two(self):
"""Second method."""
pass
def top_level_function():
"""A top-level function."""
pass
# =============================================================================
# 4. IMPORTS
# =============================================================================
# - One import per line
# - Group in order: standard library, third-party, local
# - Use absolute imports
# GOOD
import os
import sys
from typing import List, Optional
# Third party imports (after blank line)
# import numpy as np
# import pandas as pd
# Local imports (after blank line)
# from mypackage import mymodule
# BAD - Multiple imports on one line
# import os, sys
# =============================================================================
# 5. WHITESPACE
# =============================================================================
# GOOD - No extra whitespace inside brackets
spam = [1, 2, 3]
ham = {"key": "value"}
eggs = (1,)
# BAD
# spam = [ 1, 2, 3 ]
# ham = { 'key': 'value' }
# GOOD - One space around operators
x = 1
y = 2
z = x + y
# BAD - No space around = in keyword arguments
# def function(x, y = 5):
def function(x, y=5):
pass
# GOOD - Space after comma
some_list = [1, 2, 3]
# GOOD - No space before colon in slices
some_list[1:3]
some_list[::2]
# =============================================================================
# 6. NAMING CONVENTIONS
# =============================================================================
# Variables and functions: lowercase_with_underscores (snake_case)
user_name = "John"
total_count = 42
def calculate_average(numbers):
return sum(numbers) / len(numbers)
# Constants: UPPERCASE_WITH_UNDERSCORES
MAX_BUFFER_SIZE = 4096
DEFAULT_TIMEOUT = 30
PI = 3.14159
# Classes: CapitalizedWords (PascalCase)
class UserAccount:
pass
class HttpConnection:
pass
# Private: prefix with underscore
_internal_variable = "private"
def _internal_function():
pass
class MyClass:
def _protected_method(self):
"""Single underscore = protected (convention)."""
pass
def __private_method(self):
"""Double underscore = name mangling (truly private)."""
pass
# =============================================================================
# 7. COMMENTS
# =============================================================================
# GOOD - Inline comments have at least 2 spaces before #
x = 5 # This is an inline comment
# Block comments precede the code they describe
# This is a block comment that explains
# the following piece of code.
complex_calculation = 1 + 2 + 3
# Don't state the obvious
# BAD: x = 5 # Assign 5 to x
# GOOD: x = 5 # Default timeout in seconds
# =============================================================================
# 8. DOCSTRINGS
# =============================================================================
def example_function(param1, param2):
"""
Brief description of the function.
Longer description if needed. Explain what the function
does, not how it does it.
Args:
param1: Description of first parameter
param2: Description of second parameter
Returns:
Description of what is returned
Raises:
ValueError: When param1 is negative
Example:
>>> example_function(1, 2)
3
"""
return param1 + param2
class ExampleClass:
"""Brief description of the class.
Longer description of the class including its purpose
and usage patterns.
Attributes:
attr1: Description of first attribute
attr2: Description of second attribute
"""
def __init__(self, attr1, attr2):
"""Initialize ExampleClass."""
self.attr1 = attr1
self.attr2 = attr2
# =============================================================================
# 9. COMPARISON AND BOOLEAN
# =============================================================================
# Use 'is' and 'is not' for None comparisons
# GOOD
x = None
if x is None:
pass
# BAD
# if x == None:
# Use 'is not' instead of 'not ... is'
# GOOD
if x is not None:
pass
# BAD
# if not x is None:
# Don't compare boolean values with == or !=
# GOOD
flag = True
if flag:
pass
# BAD
# if flag == True:
# if flag is True: # Only use when testing identity
# =============================================================================
# 10. EXCEPTION HANDLING
# =============================================================================
# Catch specific exceptions
# GOOD
try:
value = int(user_input)
except ValueError:
print("Invalid input")
# BAD - Too broad
# try:
# value = int(user_input)
# except: # or except Exception:
# print("Error")
# Use 'raise' to re-raise exception
try:
process_data()
except ValueError:
logger.error("Bad value")
raise
# =============================================================================
# 11. FUNCTION ANNOTATIONS (Type Hints)
# =============================================================================
def greeting(name: str) -> str:
"""Return a greeting."""
return f"Hello, {name}!"
# Complex types
from typing import List, Dict, Optional
def process_items(
items: List[str],
config: Dict[str, int],
default: Optional[str] = None
) -> bool:
"""Process items with configuration."""
return True
# =============================================================================
# 12. SUMMARY: KEY RULES
# =============================================================================
print("PEP 8 Summary - Key Rules:")
print("""
1. Use 4 spaces for indentation
2. Limit lines to 79 characters
3. Use blank lines to separate functions and classes
4. Organize imports: standard lib, third-party, local
5. Use snake_case for functions and variables
6. Use PascalCase for classes
7. Use UPPER_CASE for constants
8. Use spaces around operators and after commas
9. Write docstrings for all public modules, functions, classes
10. Compare with 'is' for None, use bool directly
""")
# Use flake8 or black to automatically check/format code
print("Tools to help with PEP 8:")
print(" - flake8: Check for PEP 8 violations")
print(" - black: Automatic code formatting")
print(" - isort: Sort imports automatically")
print(" - pylint: Comprehensive code analysis")

View File

@ -0,0 +1,392 @@
"""
================================================================================
File: 02_type_hinting.py
Topic: Type Hints in Python
================================================================================
This file demonstrates type hints (type annotations) in Python. Type hints make
code more readable, enable better IDE support, and can catch bugs early using
static type checkers like mypy.
Key Concepts:
- Basic type hints
- typing module (List, Dict, Optional, etc.)
- Function annotations
- Class type hints
- Generics
- Type checking tools
Note: Type hints are OPTIONAL and don't affect runtime behavior!
================================================================================
"""
# =============================================================================
# 1. BASIC TYPE HINTS
# =============================================================================
# Syntax: variable: Type = value
print("--- Basic Type Hints ---")
# Simple types
name: str = "Alice"
age: int = 25
height: float = 1.75
is_active: bool = True
print(f"name: str = '{name}'")
print(f"age: int = {age}")
print(f"height: float = {height}")
print(f"is_active: bool = {is_active}")
# Type hints are just hints - they don't enforce types at runtime!
# This works but is wrong (would be caught by type checker):
# age: int = "not an int"
# =============================================================================
# 2. FUNCTION TYPE HINTS
# =============================================================================
# Parameters and return types
print("\n--- Function Type Hints ---")
def greet(name: str) -> str:
"""Return a greeting string."""
return f"Hello, {name}!"
def add_numbers(a: int, b: int) -> int:
"""Add two integers."""
return a + b
def is_even(n: int) -> bool:
"""Check if number is even."""
return n % 2 == 0
# Function with no return value
def print_message(message: str) -> None:
"""Print a message (returns None)."""
print(message)
print(f"greet('World'): {greet('World')}")
print(f"add_numbers(3, 5): {add_numbers(3, 5)}")
print(f"is_even(4): {is_even(4)}")
# =============================================================================
# 3. TYPING MODULE - COLLECTION TYPES
# =============================================================================
print("\n--- Collection Types ---")
from typing import List, Dict, Set, Tuple
# List of specific type
def process_numbers(numbers: List[int]) -> int:
"""Sum a list of integers."""
return sum(numbers)
# Dictionary with key and value types
def get_user_ages(users: Dict[str, int]) -> List[str]:
"""Get names of users older than 18."""
return [name for name, age in users.items() if age > 18]
# Set of specific type
def get_unique_words(text: str) -> Set[str]:
"""Get unique words from text."""
return set(text.lower().split())
# Tuple with specific types (fixed length)
def get_point() -> Tuple[float, float]:
"""Return an (x, y) coordinate."""
return (1.5, 2.5)
# Tuple with variable length of same type
def get_scores() -> Tuple[int, ...]:
"""Return any number of scores."""
return (85, 90, 78, 92)
# Examples
print(f"process_numbers([1,2,3]): {process_numbers([1, 2, 3])}")
print(f"get_unique_words('hello hello world'): {get_unique_words('hello hello world')}")
print(f"get_point(): {get_point()}")
# =============================================================================
# 4. OPTIONAL AND UNION TYPES
# =============================================================================
print("\n--- Optional and Union ---")
from typing import Optional, Union
# Optional = can be None
def find_user(user_id: int) -> Optional[str]:
"""Find user by ID, return None if not found."""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id) # Returns None if not found
# Union = can be one of several types
def process_input(value: Union[int, str]) -> str:
"""Process either int or string input."""
if isinstance(value, int):
return f"Number: {value}"
return f"String: {value}"
# Python 3.10+ syntax: X | Y instead of Union[X, Y]
# def process_input(value: int | str) -> str:
print(f"find_user(1): {find_user(1)}")
print(f"find_user(99): {find_user(99)}")
print(f"process_input(42): {process_input(42)}")
print(f"process_input('hello'): {process_input('hello')}")
# =============================================================================
# 5. SPECIAL TYPES
# =============================================================================
print("\n--- Special Types ---")
from typing import Any, Callable, Sequence, Iterable
# Any - accepts any type (avoid when possible)
def log_value(value: Any) -> None:
"""Log any value."""
print(f" Logged: {value}")
log_value(42)
log_value("hello")
log_value([1, 2, 3])
# Callable - function type
def apply_function(func: Callable[[int, int], int], a: int, b: int) -> int:
"""Apply a function to two numbers."""
return func(a, b)
print(f"\napply_function(lambda x,y: x+y, 3, 4): {apply_function(lambda x, y: x + y, 3, 4)}")
# Sequence - any ordered collection (list, tuple, str)
def get_first(items: Sequence[int]) -> int:
"""Get first item from sequence."""
return items[0]
# Iterable - anything you can iterate over
def count_items(items: Iterable[str]) -> int:
"""Count items in iterable."""
return sum(1 for _ in items)
# =============================================================================
# 6. TYPE ALIASES
# =============================================================================
print("\n--- Type Aliases ---")
from typing import List, Tuple
# Create readable aliases for complex types
UserId = int
Username = str
Coordinate = Tuple[float, float]
UserList = List[Dict[str, Union[str, int]]]
def get_user(user_id: UserId) -> Username:
"""Get username by ID."""
return f"user_{user_id}"
def calculate_distance(point1: Coordinate, point2: Coordinate) -> float:
"""Calculate distance between two points."""
return ((point2[0] - point1[0])**2 + (point2[1] - point1[1])**2)**0.5
print(f"get_user(123): {get_user(123)}")
print(f"distance((0,0), (3,4)): {calculate_distance((0, 0), (3, 4))}")
# =============================================================================
# 7. CLASS TYPE HINTS
# =============================================================================
print("\n--- Class Type Hints ---")
from typing import Optional, List, ClassVar
class Person:
"""A person with type-hinted attributes."""
# Class variables
species: ClassVar[str] = "Homo sapiens"
def __init__(self, name: str, age: int) -> None:
self.name: str = name
self.age: int = age
self.email: Optional[str] = None
def set_email(self, email: str) -> None:
"""Set the person's email."""
self.email = email
def get_info(self) -> str:
"""Get person's info."""
return f"{self.name}, {self.age} years old"
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Person":
"""Create Person from dictionary."""
return cls(data["name"], data["age"])
person = Person("Alice", 25)
person.set_email("alice@example.com")
print(f"Person: {person.get_info()}")
print(f"Email: {person.email}")
# =============================================================================
# 8. GENERICS
# =============================================================================
print("\n--- Generics ---")
from typing import TypeVar, Generic, List
# Define a type variable
T = TypeVar('T')
# Generic function
def first_item(items: List[T]) -> T:
"""Get first item of any list type."""
return items[0]
# Generic class
class Stack(Generic[T]):
"""A generic stack."""
def __init__(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
"""Push item onto stack."""
self._items.append(item)
def pop(self) -> T:
"""Pop item from stack."""
return self._items.pop()
def is_empty(self) -> bool:
"""Check if stack is empty."""
return len(self._items) == 0
# Using generics
print(f"first_item([1, 2, 3]): {first_item([1, 2, 3])}")
print(f"first_item(['a', 'b']): {first_item(['a', 'b'])}")
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(f"Stack pop: {stack.pop()}")
# =============================================================================
# 9. LITERAL AND FINAL
# =============================================================================
print("\n--- Literal and Final ---")
from typing import Literal, Final
# Literal - exact values only
Mode = Literal["r", "w", "a"]
def open_file(path: str, mode: Mode) -> str:
"""Open file with specific mode."""
return f"Opening {path} in mode '{mode}'"
print(open_file("data.txt", "r"))
# open_file("data.txt", "x") # Type error! "x" is not allowed
# Final - cannot be reassigned
MAX_SIZE: Final[int] = 100
# MAX_SIZE = 200 # Type checker would flag this
print(f"MAX_SIZE (Final): {MAX_SIZE}")
# =============================================================================
# 10. TYPE CHECKING TOOLS
# =============================================================================
print("\n--- Type Checking ---")
print("""
Type hints are NOT enforced at runtime!
Use these tools to check types statically:
1. mypy - The most popular type checker
pip install mypy
mypy your_script.py
2. pyright - Microsoft's type checker (fast)
pip install pyright
pyright your_script.py
3. IDE Support
- VS Code with Pylance
- PyCharm (built-in)
Example mypy output:
error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Benefits of type hints:
Catch bugs early (before runtime)
Better IDE autocomplete
Self-documenting code
Easier refactoring
Better code reviews
""")
# =============================================================================
# 11. BEST PRACTICES
# =============================================================================
print("--- Best Practices ---")
print("""
1. Start with function signatures
- Hint parameters and return types first
2. Use Optional[X] for nullable values
- Not: x: str = None (wrong type!)
- Yes: x: Optional[str] = None
3. Prefer specific types over Any
- Any defeats the purpose of type hints
4. Use type aliases for complex types
- Makes code more readable
5. Enable strict mode in mypy
- mypy --strict your_script.py
6. Type hint public APIs first
- Internal code can be less strict
7. Use TypedDict for dictionaries with known structure
8. Consider using dataclasses for structured data
""")
# Example: TypedDict and dataclass
from typing import TypedDict
from dataclasses import dataclass
class UserDict(TypedDict):
"""User data as dictionary."""
name: str
age: int
email: Optional[str]
@dataclass
class UserDataClass:
"""User data as dataclass."""
name: str
age: int
email: Optional[str] = None
# Both provide type safety for structured data
user_dict: UserDict = {"name": "Alice", "age": 25, "email": None}
user_obj = UserDataClass(name="Bob", age=30)
print(f"\nTypedDict: {user_dict}")
print(f"Dataclass: {user_obj}")

View File

@ -0,0 +1,455 @@
"""
================================================================================
File: 03_virtual_envs.py
Topic: Virtual Environments in Python
================================================================================
This file explains virtual environments in Python. Virtual environments are
isolated Python environments that allow you to manage dependencies separately
for each project, avoiding conflicts between different projects.
Key Concepts:
- What are virtual environments
- Creating and activating venvs
- Managing dependencies with pip
- requirements.txt
- Best practices
Note: This is a reference/tutorial file - the commands are shown as examples.
================================================================================
"""
# =============================================================================
# 1. WHAT ARE VIRTUAL ENVIRONMENTS?
# =============================================================================
print("=== What Are Virtual Environments? ===")
print("""
A virtual environment is an ISOLATED Python environment that:
Has its own Python interpreter
Has its own site-packages directory
Doesn't affect global Python installation
Allows different projects to have different dependencies
Why use virtual environments?
- Project A needs Django 3.2
- Project B needs Django 4.1
- Without venvs, you'd have conflicts!
With virtual environments, each project has its own Django version.
""")
# =============================================================================
# 2. CREATING A VIRTUAL ENVIRONMENT
# =============================================================================
print("\n=== Creating a Virtual Environment ===")
print("""
Using the built-in 'venv' module (Python 3.3+):
# Navigate to your project directory
cd my_project
# Create a virtual environment named 'venv'
python -m venv venv
# Or name it something else
python -m venv .venv # Hidden folder (common convention)
python -m venv env # Another common name
python -m venv my_env # Custom name
This creates a folder with:
venv/
Include/ # C header files
Lib/ # Python packages
site-packages/
Scripts/ # Activation scripts (Windows)
activate
activate.bat
python.exe
pyvenv.cfg # Configuration file
""")
# =============================================================================
# 3. ACTIVATING AND DEACTIVATING
# =============================================================================
print("\n=== Activating and Deactivating ===")
print("""
ACTIVATION (must do before using the venv):
Windows (Command Prompt):
venv\\Scripts\\activate.bat
Windows (PowerShell):
venv\\Scripts\\Activate.ps1
If you get execution policy error:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
macOS/Linux:
source venv/bin/activate
When activated, your prompt changes:
(venv) C:\\my_project>
(venv) user@computer:~/my_project$
DEACTIVATION:
deactivate
After deactivation, prompt returns to normal.
""")
# =============================================================================
# 4. INSTALLING PACKAGES
# =============================================================================
print("\n=== Installing Packages ===")
print("""
After activating your venv, use pip to install packages:
# Install a package
pip install requests
# Install specific version
pip install requests==2.28.0
# Install minimum version
pip install 'requests>=2.25.0'
# Install multiple packages
pip install requests numpy pandas
# Install from requirements.txt
pip install -r requirements.txt
# Upgrade a package
pip install --upgrade requests
# Uninstall a package
pip uninstall requests
# List installed packages
pip list
# Show package info
pip show requests
""")
# =============================================================================
# 5. REQUIREMENTS FILE
# =============================================================================
print("\n=== Requirements File ===")
print("""
The requirements.txt file lists all project dependencies:
CREATING requirements.txt:
# Generate from current environment
pip freeze > requirements.txt
EXAMPLE requirements.txt:
# requirements.txt
requests==2.28.1
numpy>=1.21.0
pandas~=1.5.0
python-dotenv
VERSION SPECIFIERS:
package==1.0.0 # Exact version
package>=1.0.0 # Minimum version
package<=1.0.0 # Maximum version
package~=1.0.0 # Compatible version (>=1.0.0, <2.0.0)
package!=1.0.0 # Exclude version
package>=1.0,<2.0 # Version range
INSTALLING FROM requirements.txt:
pip install -r requirements.txt
BEST PRACTICE: Create different files for different environments:
requirements.txt # Production dependencies
requirements-dev.txt # Development dependencies
requirements-test.txt # Testing dependencies
""")
# =============================================================================
# 6. VIRTUAL ENVIRONMENT ALTERNATIVES
# =============================================================================
print("\n=== Alternative Tools ===")
print("""
1. venv (built-in)
- Simple, included with Python
- Good for basic needs
python -m venv venv
2. virtualenv
- More features than venv
- Faster environment creation
pip install virtualenv
virtualenv venv
3. pipenv
- Combines venv + pip
- Uses Pipfile instead of requirements.txt
- Automatic locking of dependencies
pip install pipenv
pipenv install requests
pipenv shell
4. poetry
- Modern dependency management
- Better dependency resolution
- Build and publish packages
pip install poetry
poetry new my_project
poetry add requests
5. conda
- Package manager + environment manager
- Great for data science (NumPy, SciPy pre-compiled)
conda create -n myenv python=3.10
conda activate myenv
conda install numpy
""")
# =============================================================================
# 7. PROJECT STRUCTURE WITH VENV
# =============================================================================
print("\n=== Project Structure ===")
print("""
Recommended project structure:
my_project/
venv/ # Virtual environment (don't commit!)
src/
my_package/
__init__.py
main.py
tests/
__init__.py
test_main.py
.gitignore # Include 'venv/' here!
requirements.txt
requirements-dev.txt
setup.py or pyproject.toml
README.md
.gitignore should include:
venv/
.venv/
env/
__pycache__/
*.pyc
.pytest_cache/
""")
# =============================================================================
# 8. COMMON COMMANDS REFERENCE
# =============================================================================
print("\n=== Quick Reference ===")
print("""
VIRTUAL ENVIRONMENT COMMANDS
CREATE
python -m venv venv
ACTIVATE
Windows: venv\\Scripts\\activate
Mac/Linux: source venv/bin/activate
DEACTIVATE
deactivate
INSTALL PACKAGES
pip install package_name
pip install -r requirements.txt
EXPORT DEPENDENCIES
pip freeze > requirements.txt
CHECK PYTHON LOCATION
Windows: where python
Mac/Linux: which python
DELETE VENV (just remove the folder)
Windows: rmdir /s /q venv
Mac/Linux: rm -rf venv
""")
# =============================================================================
# 9. BEST PRACTICES
# =============================================================================
print("\n=== Best Practices ===")
print("""
1. ALWAYS use virtual environments
- Even for small projects
- Prevents "works on my machine" problems
2. Don't commit venv to version control
- Add 'venv/' to .gitignore
- Share requirements.txt instead
3. Use meaningful venv names
- venv, .venv, or env are common
- Use .venv to hide the folder
4. Pin your versions in production
- Use pip freeze > requirements.txt
- Review and clean up before committing
5. Separate dev and production dependencies
- requirements.txt for production
- requirements-dev.txt for testing, linting, etc.
6. Document how to set up the environment
- Include setup instructions in README.md
7. Use pip-tools for better dependency management
- pip install pip-tools
- Create requirements.in with direct dependencies
- pip-compile requirements.in creates pinned file
""")
# =============================================================================
# 10. TROUBLESHOOTING
# =============================================================================
print("\n=== Troubleshooting ===")
print("""
COMMON ISSUES AND SOLUTIONS:
1. "python not recognized" after activation
Solution: Use full path or reinstall Python with PATH option
2. PowerShell "execution policy" error
Solution:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
3. "pip install" uses global pip
Solution: Make sure venv is activated first
Check: python -c "import sys; print(sys.prefix)"
4. Can't delete venv folder (files in use)
Solution:
- Deactivate first
- Close any terminals/IDEs using it
- Restart computer if needed
5. requirements.txt has too many packages
Solution: Use pipreqs to generate minimal requirements
pip install pipreqs
pipreqs /path/to/project
6. Package conflicts
Solution: Create fresh venv and install carefully
Or use pip-compile for dependency resolution
""")
# =============================================================================
# 11. IDE INTEGRATION
# =============================================================================
print("\n=== IDE Integration ===")
print("""
VS CODE:
1. Open project folder
2. Select Python Interpreter (Ctrl+Shift+P)
3. Choose "Python: Select Interpreter"
4. Select the venv Python (./venv/Scripts/python.exe)
5. New terminals auto-activate venv
PYCHARM:
1. File > Settings > Project > Python Interpreter
2. Click gear icon > Add
3. Select "Existing environment" or create new
4. Point to venv/Scripts/python.exe
JUPYTER NOTEBOOK:
1. Activate venv
2. Install: pip install ipykernel
3. Register: python -m ipykernel install --user --name=myenv
4. Select kernel in Jupyter
""")
# =============================================================================
# 12. EXAMPLE WORKFLOW
# =============================================================================
print("\n=== Example Workflow ===")
print("""
STARTING A NEW PROJECT:
# 1. Create project folder
mkdir my_awesome_project
cd my_awesome_project
# 2. Create virtual environment
python -m venv venv
# 3. Activate it
venv\\Scripts\\activate # Windows
source venv/bin/activate # Mac/Linux
# 4. Upgrade pip (good practice)
python -m pip install --upgrade pip
# 5. Install packages
pip install requests numpy pandas
# 6. Create requirements.txt
pip freeze > requirements.txt
# 7. Create .gitignore
echo "venv/" > .gitignore
echo "__pycache__/" >> .gitignore
# 8. Start coding!
code .
JOINING AN EXISTING PROJECT:
# 1. Clone the repository
git clone https://github.com/user/project.git
cd project
# 2. Create virtual environment
python -m venv venv
# 3. Activate it
venv\\Scripts\\activate # Windows
# 4. Install dependencies
pip install -r requirements.txt
# 5. Start working!
""")
print("\n" + "=" * 60)
print("Virtual environments are essential for Python development!")
print("Always use them to keep your projects isolated and reproducible.")
print("=" * 60)

130
README.md Normal file
View File

@ -0,0 +1,130 @@
# Python Review - Complete Tutorial
A comprehensive Python tutorial covering everything from basics to advanced topics.
Each file is self-contained with detailed explanations and executable examples.
## 📚 Topics Covered
### 01. Basics
| File | Topic |
|------|-------|
| `01_print.py` | Print function, formatting, escape characters |
| `02_comments.py` | Single-line, multi-line comments, docstrings |
| `03_variables.py` | Variable assignment, naming, multiple assignment |
| `04_data_types.py` | Numbers, strings, booleans, type conversion |
### 02. Control Flow
| File | Topic |
|------|-------|
| `01_if_else.py` | Conditionals, comparison operators, logical operators |
| `02_elif.py` | Multiple conditions, grading systems, ranges |
| `03_match_case.py` | Pattern matching (Python 3.10+), guards, unpacking |
### 03. Loops
| File | Topic |
|------|-------|
| `01_for_loop.py` | For loops, range(), enumerate(), zip() |
| `02_while_loop.py` | While loops, counters, sentinels, infinite loops |
| `03_break_continue.py` | Loop control, break, continue, pass, for-else |
### 04. Data Structures
| File | Topic |
|------|-------|
| `01_lists.py` | Lists, indexing, slicing, methods, comprehensions |
| `02_tuples.py` | Tuples, immutability, unpacking, named tuples |
| `03_sets.py` | Sets, operations, frozen sets, practical uses |
| `04_dictionaries.py` | Dictionaries, methods, nesting, comprehensions |
### 05. Functions
| File | Topic |
|------|-------|
| `01_function_basics.py` | Defining functions, scope, docstrings, nested functions |
| `02_arguments.py` | Positional, keyword, *args, **kwargs, type hints |
| `03_return_values.py` | Returns, multiple returns, early returns, generators |
| `04_lambda_functions.py` | Lambda, map, filter, reduce, functional patterns |
### 06. Modules & Packages
| File | Topic |
|------|-------|
| `01_imports.py` | Import patterns, standard library, module organization |
| `02_custom_modules.py` | Creating modules, packages, __init__.py, __name__ |
### 07. Error Handling
| File | Topic |
|------|-------|
| `01_try_except.py` | try/except/else/finally, catching exceptions |
| `02_custom_exceptions.py` | Creating exceptions, hierarchy, best practices |
### 08. Object-Oriented Programming
| File | Topic |
|------|-------|
| `01_classes_objects.py` | Classes, objects, attributes, methods, self |
| `02_init_methods.py` | Constructors, properties, classmethods |
| `03_inheritance.py` | Single/multiple inheritance, super(), MRO, ABC |
| `04_polymorphism.py` | Duck typing, operator overloading, protocols |
### 09. Advanced Python
| File | Topic |
|------|-------|
| `01_list_comprehensions.py` | List, dict, set comprehensions, generators |
| `02_generators.py` | Yield, iterators, memory efficiency, pipelines |
| `03_decorators.py` | Simple/parameterized decorators, functools |
| `04_context_managers.py` | with statement, __enter__/__exit__, contextlib |
### 10. Best Practices
| File | Topic |
|------|-------|
| `01_pep8.py` | Python style guide, naming, formatting |
| `02_type_hinting.py` | Type annotations, typing module, generics |
| `03_virtual_envs.py` | venv, pip, requirements.txt, project setup |
## 🚀 How to Use
1. **Start from basics**: Begin with `01_basics` and work your way up
2. **Run the files**: Each file is executable: `python 01_print.py`
3. **Read the comments**: Each section is thoroughly explained
4. **Experiment**: Modify the code and observe the results
## 🛠️ Prerequisites
- Python 3.10+ recommended (for match/case support)
- Python 3.8+ minimum (for most features)
## 📝 File Structure
Each file follows this structure:
```python
"""
================================================================================
File: filename.py
Topic: Topic Name
================================================================================
Description of what the file covers...
Key Concepts:
- Concept 1
- Concept 2
- ...
================================================================================
"""
# Section 1: Basic Example
# ... code with inline comments ...
# Section 2: Advanced Example
# ... more code ...
```
## ✅ Learning Path
```
Beginner: 01_basics → 02_control_flow → 03_loops
Intermediate: 04_data_structures → 05_functions → 06_modules
Advanced: 07_error_handling → 08_oop → 09_advanced
Professional: 10_best_practices
```
---
*Happy Learning! 🐍*