Codcups
Progress: 0%

01. ๐Ÿš€ Introduction & Setup

Python is a high-level, interpreted language known for its simplicity and readability. Created by Guido van Rossum in 1991, Python has grown to become one of the most popular programming languages worldwide, powering everything from web applications and data analysis to artificial intelligence and scientific computing.

Why Learn Python?

Python's popularity stems from several key advantages:
1. **Beginner-friendly**: Clean syntax that reads like English
2. **Versatile**: Used in web development (Django, Flask), data science (Pandas, NumPy), machine learning (TensorFlow, PyTorch), automation, and more
3. **Large community**: Extensive documentation and third-party libraries
4. **Cross-platform**: Runs on Windows, macOS, Linux, and even microcontrollers
5. **High demand**: Consistently ranked among the most sought-after skills in tech

The Zen of Python

Python's design philosophy is encapsulated in the "Zen of Python," which you can view by running import this. These 19 guiding principles influence how Python code should be written and organized. Some of the most important principles include:

  • Beautiful is better than ugly. - Code aesthetics matter for readability and maintainability.
  • Explicit is better than implicit. - Code should be clear about what it's doing.
  • Simple is better than complex. - Favor straightforward solutions over convoluted ones.
  • Complex is better than complicated. - When complexity is necessary, make it understandable.
  • Readability counts. - Code is read more often than it's written.
  • There should be oneโ€”and preferably only oneโ€”obvious way to do it. - Promotes consistency across Python codebases.

Python Versions: 2 vs 3

Python 2 was officially discontinued in 2020. Python 3 (specifically 3.6+) is now the standard. Key differences include:
- Print became a function: print "hello" โ†’ print("hello")
- Integer division changed: 3/2 = 1 (Python 2) โ†’ 3/2 = 1.5 (Python 3)
- Unicode by default (better international support)
- Improved syntax and performance
Always use Python 3 for new projects!

**Interpreted vs Compiled**: Python is interpreted, meaning code is executed line-by-line by the Python interpreter. This allows for rapid development and debugging, though it can be slower than compiled languages like C++ for certain tasks.

Example: The Classic Hello World and More

# A single line is all it takes!
print("Hello, Python Master!")

# Python can also do quick calculations
print("5 + 3 =", 5 + 3)

# Variables work without type declarations
message = "Welcome to Python programming"
print(message)

# Multi-line strings with triple quotes
multi_line = """This is a
multi-line string
in Python"""
print(multi_line)

**Note:** Python uses indentation (usually 4 spaces) to define code blocks, unlike other languages that use braces (`{}`). Consistent indentation is not just good practiceโ€”it's required for the code to work correctly!

Sample Output:
Hello, Python Master!
5 + 3 = 8
Welcome to Python programming
This is a
multi-line string
in Python

This output demonstrates Python's interactive nature - you can see results immediately after running code.

02. ๐Ÿ“ฆ Variables & Data Types

Variables are symbolic names that reference values stored in computer memory. Think of them as labeled boxes where you can store different types of data. Python is **dynamically typed**, meaning you don't need to explicitly declare the variable's typeโ€”Python infers it from the assigned value.

Naming Conventions and Rules

Python variable names:
1. Can contain letters, numbers, and underscores
2. Cannot start with a number
3. Are case-sensitive (age and Age are different)
4. Cannot be Python keywords (if, for, while, etc.)
5. Follow snake_case convention for readability: user_name not userName
Good naming makes code self-documenting. Instead of x = 10, use student_count = 10.

Python's Built-in Data Types

Python comes with several built-in data types, each optimized for specific kinds of data:

  • Numeric Types:
    • int - Integer (whole numbers): 42, -7, 0
    • float - Floating point (decimals): 3.14, -0.001, 2.0
    • complex - Complex numbers: 3+4j
  • Sequence Types:
    • str - String (text): "hello", 'world'
    • list - Ordered, mutable collection: [1, 2, 3]
    • tuple - Ordered, immutable collection: (1, 2, 3)
  • Mapping Type: dict - Key-value pairs: {"name": "Alice", "age": 30}
  • Set Types: set and frozenset - Unordered collections of unique elements
  • Boolean Type: bool - Logical values: True or False
  • Binary Types: bytes, bytearray, memoryview - For handling binary data

Understanding Mutable vs Immutable

This is a crucial Python concept:
Immutable types cannot be changed after creation: int, float, str, tuple, bool
Mutable types can be modified: list, dict, set
Example: x = "hello" โ†’ x[0] = "H" causes an error (string is immutable)
Example: y = [1, 2, 3] โ†’ y[0] = 100 works fine (list is mutable)

**Type Casting:** You can convert between types using constructor functions like int(), float(), str(), list(), etc. This is essential when you need to change the type of data, like converting user input (which comes as a string) to a number for calculations.

Example: Dynamic Typing, Type Checking, and Casting

# Dynamic assignment - Python figures out the type
age = 30           # Python knows this is an int
temperature = 25.5 # Python knows this is a float
name = "Charlie"   # Python knows this is a str
is_student = True  # Python knows this is a bool

# Checking type with type() function
print(f"Type of age: {type(age)}")          # 
print(f"Type of temperature: {type(temperature)}") # 
print(f"Type of name: {type(name)}")        # 

# Type Casting Examples
str_number = "100"
int_number = int(str_number)     # Convert string to integer
float_number = float(int_number) # Convert integer to float
back_to_str = str(float_number)  # Convert float back to string

print(f"Original string: '{str_number}'")
print(f"As integer: {int_number}")
print(f"As float: {float_number}")
print(f"Back to string: '{back_to_str}'")

# Common casting scenarios
user_input = "42.7"  # Input from user is always string
price = float(user_input)  # Convert to float for calculations
discounted_price = price * 0.9  # Now we can do math
print(f"Discounted price: ${discounted_price:.2f}")

# Boolean casting (truthy/falsy values)
print(f"bool(0): {bool(0)}")        # False
print(f"bool(1): {bool(1)}")        # True  
print(f"bool(''): {bool('')}")      # False (empty string)
print(f"bool('hello'): {bool('hello')}") # True (non-empty string)

Truthy and Falsy Values

In Python, every value can be evaluated as either True or False in a boolean context. This is useful in conditionals:
Falsy values: False, None, zero of any numeric type (0, 0.0, 0j), empty sequences ('', (), []), empty mappings ({})
Truthy values: Everything else
Example: if user_input: checks if user_input is not empty.

Variable Type Demonstration:

Integer

count = 42

Whole numbers, positive or negative

Float

pi = 3.14159

Decimal numbers with floating point

String

name = "Python"

Text data, can use single or double quotes

Boolean

is_valid = True

Logical values: True or False

03. ๐Ÿšฆ Control Flow (If/Else/Elif)

Conditional statements allow your program to make decisions and execute different code paths based on whether conditions are True or False. This is fundamental to creating intelligent, responsive programs that can handle different situations.

The Anatomy of Python Conditionals

Python's conditional structure is clean and readable:
1. Start with if followed by a condition
2. Add elif (short for "else if") for additional conditions
3. End with else for a catch-all case
4. Each clause ends with a colon (:)
5. The code to execute is indented (usually 4 spaces)
Python doesn't use braces or parentheses for blocksโ€”indentation defines what belongs to each condition.

Comparison and Logical Operators

Conditions are built using operators that compare values or combine multiple conditions:

  • Comparison Operators:
    • == Equal to (Note: single = is for assignment!)
    • != Not equal to
    • > Greater than
    • < Less than
    • >= Greater than or equal to
    • <= Less than or equal to
  • Logical Operators:
    • and Both conditions must be True
    • or At least one condition must be True
    • not Reverses the boolean value
  • Membership Operators:
    • in Value exists in sequence
    • not in Value doesn't exist in sequence
  • Identity Operators:
    • is Both variables point to same object
    • is not Variables point to different objects

Common Pitfalls with Conditionals

Watch out for these common mistakes:
1. Using = (assignment) instead of == (comparison)
2. Forgetting the colon (:) after if, elif, else
3. Inconsistent indentation (mixing tabs and spaces)
4. Checking equality with floating-point numbers (use tolerance instead: abs(a - b) < 0.0001)
5. Chaining comparisons properly: 0 < x < 10 works in Python!

**The Ternary Operator**: Python has a concise one-line if-else expression: value_if_true if condition else value_if_false. This is useful for simple assignments but can hurt readability if overused for complex logic.

Example: Comprehensive Conditional Logic

# Basic if-elif-else structure
temperature = 28
weather = "sunny"

if temperature > 30:
    print("It's a hot day! Stay hydrated.")
elif temperature > 25 and weather == "sunny":
    print("Perfect beach weather!")
elif temperature > 20 or weather == "cloudy":
    print("Good day for outdoor activities.")
else:
    print("Maybe stay indoors today.")

# Nested conditionals (if inside if)
age = 25
has_license = True

if age >= 18:
    print("You're an adult.")
    if has_license:
        print("You can drive a car.")
    else:
        print("You need to get a license.")
else:
    print("You're a minor.")

# Ternary Operator (Conditional Expression)
score = 85
grade = "Pass" if score >= 60 else "Fail"
print(f"Score: {score}, Grade: {grade}")

# Chained comparisons (Python special feature!)
x = 15
if 10 <= x <= 20:  # Same as: if x >= 10 and x <= 20
    print("x is between 10 and 20")

# Membership testing with 'in'
fruits = ["apple", "banana", "orange"]
if "banana" in fruits:
    print("We have bananas!")
if "grape" not in fruits:
    print("We're out of grapes.")

# Truthy/Falsy evaluation
user_input = ""
if user_input:  # Empty string is Falsy
    print(f"You entered: {user_input}")
else:
    print("No input provided")

# Complex logical conditions
is_weekend = False
has_money = True
weather_good = True

if (is_weekend or has_money) and weather_good:
    print("Let's go out!")
else:
    print("Stay home.")

Short-Circuit Evaluation

Python uses short-circuit evaluation with and and or:
- For a and b: If a is False, Python doesn't evaluate b
- For a or b: If a is True, Python doesn't evaluate b
This is efficient and allows for safe checks:
if user is not None and user.is_active:
Won't cause error if user is None.

Conditional Flow Visualization:
Start: temperature = 28, weather = "sunny"
if temperature > 30: โŒ False
elif temperature > 25 and weather == "sunny": โœ… True
elif temperature > 20 or weather == "cloudy": Skipped (previous condition was True)
else: Skipped
Output: "Perfect beach weather!"

04. ๐Ÿ“œ Lists & Tuples

Lists and tuples are Python's primary sequence data types for storing ordered collections of items. They are similar in many ways but have one crucial difference: mutability.

Understanding Mutability

Mutability refers to whether an object can be changed after creation:
- Lists are mutable: You can add, remove, or change elements after creation
- Tuples are immutable: Once created, they cannot be modified
This difference has important implications for performance and safety. Tuples are faster and use less memory than lists. Use tuples for data that shouldn't change (like coordinates, configuration settings, or database records). Use lists for data that needs to be modified (like user inputs, dynamic collections).

List Operations and Methods

Lists come with a rich set of built-in methods for manipulation:

  • Adding elements: append(), insert(), extend()
  • Removing elements: remove(), pop(), clear()
  • Searching: index(), count()
  • Sorting: sort(), reverse()
  • Copying: copy(), list() constructor, slicing [:]

List Comprehension: Python's Superpower

List comprehensions provide a concise way to create lists. Instead of using loops, you can generate lists in a single line:
Traditional way:
squares = []
for i in range(10):
    squares.append(i**2)


List comprehension way:
squares = [i**2 for i in range(10)]

You can also add conditions:
even_squares = [i**2 for i in range(10) if i % 2 == 0]

Tuple Characteristics

Tuples have several unique features:

  • Immutable: Cannot be changed after creation
  • Faster than lists: For iteration and accessing elements
  • Hashable: Can be used as dictionary keys (lists cannot)
  • Packing/unpacking: Multiple assignment feature
  • Single-element tuples: Require a trailing comma: (5,)

**Indexing and Slicing**: Both lists and tuples support indexing (my_list[0]) and slicing (my_list[1:4]). Python uses zero-based indexing, and negative indices count from the end (my_list[-1] gets the last element).

Example: Comprehensive List and Tuple Operations

# ========== LISTS ==========
# Creating lists
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True]  # Lists can hold different types

# List methods demonstration
print("Original fruits list:", fruits)

# Adding elements
fruits.append("orange")  # Add to end
print("After append:", fruits)

fruits.insert(1, "blueberry")  # Insert at specific position
print("After insert:", fruits)

more_fruits = ["mango", "pineapple"]
fruits.extend(more_fruits)  # Add multiple elements
print("After extend:", fruits)

# Removing elements
fruits.remove("banana")  # Remove by value
print("After remove:", fruits)

last_fruit = fruits.pop()  # Remove and return last element
print(f"Popped: {last_fruit}, Remaining: {fruits}")

second_fruit = fruits.pop(1)  # Remove by index
print(f"Popped index 1: {second_fruit}")

# Sorting
fruits.sort()  # Alphabetical order
print("Sorted:", fruits)

fruits.sort(reverse=True)  # Reverse alphabetical
print("Reverse sorted:", fruits)

# List comprehension examples
squares = [x**2 for x in range(1, 6)]
print("Squares:", squares)  # [1, 4, 9, 16, 25]

even_squares = [x**2 for x in range(1, 11) if x % 2 == 0]
print("Even squares:", even_squares)  # [4, 16, 36, 64, 100]

# Slicing
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print("First 3:", numbers[:3])      # [0, 1, 2]
print("Last 3:", numbers[-3:])      # [7, 8, 9]
print("Every other:", numbers[::2]) # [0, 2, 4, 6, 8]
print("Reverse:", numbers[::-1])    # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

# ========== TUPLES ==========
# Creating tuples
coordinates = (10.5, 20.7)
rgb_color = (255, 0, 128)
single_element = (42,)  # Note the comma!

print("\nTuple examples:")
print(f"Coordinates: {coordinates}")
print(f"First coordinate: {coordinates[0]}")
print(f"Last coordinate: {coordinates[-1]}")

# Tuple unpacking (multiple assignment)
x, y = coordinates
print(f"Unpacked: x={x}, y={y}")

# Swapping variables using tuple unpacking
a, b = 5, 10
print(f"Before swap: a={a}, b={b}")
a, b = b, a  # Pythonic way to swap
print(f"After swap: a={a}, b={b}")

# Tuples as dictionary keys (lists cannot do this!)
location_data = {
    (40.7128, -74.0060): "New York",
    (51.5074, -0.1278): "London"
}
print(f"NYC coordinates: {location_data[(40.7128, -74.0060)]}")

# Attempting to modify a tuple (will cause error)
try:
    coordinates[0] = 15.0  # This will raise TypeError
except TypeError as e:
    print(f"Tuple modification error: {e}")

Memory and Performance Considerations

Lists and tuples have different memory footprints and performance characteristics:
- Memory: Tuples use less memory than lists of the same size
- Creation speed: Tuples are faster to create than lists
- Access speed: Both have O(1) access time for indexing
- Modification: Lists can be modified, tuples cannot
- Hashability: Tuples are hashable (can be dictionary keys), lists are not
Use tuples for fixed data, lists for dynamic collections.

Visual Comparison: Lists vs Tuples

๐Ÿ“ Lists (Mutable)

  • โœ… Can add/remove items
  • โœ… Can modify existing items
  • โœ… Use for dynamic data
  • โŒ Cannot be dictionary keys
  • โŒ More memory usage
  • Syntax: [ ]

๐Ÿ“‹ Tuples (Immutable)

  • โŒ Cannot add/remove items
  • โŒ Cannot modify existing items
  • โœ… Use for fixed data
  • โœ… Can be dictionary keys
  • โœ… Less memory usage
  • Syntax: ( )

๐Ÿ’ก Tip: Use tuples when the data shouldn't change (coordinates, settings). Use lists when you need to modify the collection (shopping cart, user inputs).

05. ๐Ÿ” Iteration (For and While Loops)

Loops are fundamental programming constructs that allow you to execute a block of code repeatedly. Python provides two main types of loops: for loops for iterating over sequences, and while loops for repeating code while a condition is true.

Understanding Iterables

In Python, an iterable is any object that can return its elements one at a time. Common iterables include:
- Lists, tuples, strings
- Dictionaries (iterates over keys by default)
- Sets
- Range objects
- File objects
- Custom objects that implement the __iter__() method
The for loop works with any iterable, making it extremely versatile.

For Loops: The Workhorse of Python

The for loop is Python's primary tool for iteration. Its basic syntax is:

for item in iterable:
    # Code to execute for each item

Common patterns with for loops:

  • Iterating with index: Use enumerate() to get both index and value
  • Iterating multiple sequences: Use zip() to iterate over multiple lists simultaneously
  • Range-based iteration: Use range() for numerical loops
  • Dictionary iteration: Use items(), keys(), or values() methods

The range() Function

range() generates a sequence of numbers and is commonly used with for loops. It has three forms:
1. range(stop): 0 to stop-1
2. range(start, stop): start to stop-1
3. range(start, stop, step): start to stop-1 with specified step
Important: range() doesn't create a list in memory (Python 3). It generates numbers on the fly, making it memory efficient even for large ranges.

While Loops: Conditional Iteration

While loops continue executing as long as a condition remains True. The syntax is:

while condition:
    # Code to execute while condition is True

**Loop Control Statements**: break immediately exits the loop, continue skips to the next iteration, and pass does nothing (used as a placeholder). The else clause after a loop executes only if the loop completes normally (without a break).

Example: Comprehensive Loop Examples

# ========== FOR LOOPS ==========
print("=== For Loop Examples ===")

# Basic for loop over a list
fruits = ["apple", "banana", "cherry", "date"]
print("Fruits:")
for fruit in fruits:
    print(f"  - {fruit}")

# Using enumerate() to get index and value
print("\nFruits with index:")
for index, fruit in enumerate(fruits):
    print(f"  {index}: {fruit}")

# Using enumerate with start parameter
print("\nFruits with custom starting index:")
for index, fruit in enumerate(fruits, start=1):
    print(f"  {index}. {fruit}")

# Iterating with range()
print("\nCounting with range():")
for i in range(5):  # 0 to 4
    print(f"  Number: {i}")

print("\nCounting from 1 to 5:")
for i in range(1, 6):  # 1 to 5
    print(f"  Number: {i}")

print("\nEven numbers from 0 to 10:")
for i in range(0, 11, 2):  # Start at 0, stop before 11, step by 2
    print(f"  {i}")

# Iterating over dictionaries
student = {"name": "Alice", "age": 21, "major": "Computer Science"}
print("\nDictionary iteration:")

print("Keys:")
for key in student.keys():
    print(f"  {key}")

print("\nValues:")
for value in student.values():
    print(f"  {value}")

print("\nKey-Value pairs:")
for key, value in student.items():
    print(f"  {key}: {value}")

# Using zip() to iterate multiple lists
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
print("\nZipped iteration:")
for name, score in zip(names, scores):
    print(f"  {name}: {score}")

# ========== WHILE LOOPS ==========
print("\n=== While Loop Examples ===")

# Basic while loop
count = 0
print("Counting from 0 to 4:")
while count < 5:
    print(f"  Count: {count}")
    count += 1  # Don't forget to update the condition!

# While loop with break
print("\nBreaking early:")
attempts = 0
while True:  # Infinite loop (be careful!)
    attempts += 1
    print(f"  Attempt {attempts}")
    if attempts >= 3:
        print("  Too many attempts, breaking!")
        break

# While loop with continue
print("\nSkipping even numbers (1-10):")
num = 0
while num < 10:
    num += 1
    if num % 2 == 0:
        continue  # Skip even numbers
    print(f"  Odd number: {num}")

# ========== LOOP CONTROL ==========
print("\n=== Loop Control Examples ===")

# The else clause in loops
print("Searching for 'banana':")
for fruit in fruits:
    if fruit == "banana":
        print(f"  Found {fruit}!")
        break
else:
    # Executes only if loop completes without break
    print("  Banana not found")

print("\nSearching for 'grape':")
for fruit in fruits:
    if fruit == "grape":
        print(f"  Found {fruit}!")
        break
else:
    print("  Grape not found (else clause executed)")

# Nested loops
print("\nMultiplication table (1-3):")
for i in range(1, 4):
    for j in range(1, 4):
        print(f"  {i} ร— {j} = {i * j}")
    print()  # Empty line between tables

# ========== PRACTICAL EXAMPLES ==========
print("\n=== Practical Loop Applications ===")

# Processing user input until valid
print("Enter a positive number:")
while True:
    try:
        user_input = input("> ")
        number = float(user_input)
        if number > 0:
            print(f"  Valid number: {number}")
            break
        else:
            print("  Please enter a positive number")
    except ValueError:
        print("  That's not a valid number!")

# Summing numbers in a list
numbers = [1, 2, 3, 4, 5]
total = 0
for num in numbers:
    total += num
print(f"\nSum of {numbers} = {total}")

# Finding maximum value
max_val = numbers[0]
for num in numbers:
    if num > max_val:
        max_val = num
print(f"Maximum value in {numbers} = {max_val}")

Choosing Between For and While Loops

Use for loops when:
- You know how many times to iterate (or are iterating over a collection)
- You need to process each item in a sequence
- You want cleaner, more readable code for iteration

Use while loops when:
- You don't know how many iterations are needed
- You're waiting for a condition to become False
- You need infinite loops (with break conditions)
- You're reading data until a sentinel value appears

Pro tip: Most for loops can be rewritten as while loops, but for loops are generally more Pythonic and less error-prone.

Loop Execution Flow:
For Loop Execution
1
Initialize: Get first item from sequence
2
Execute loop body with current item
3
Get next item from sequence
4
Repeat until no items left
While Loop Execution
1
Check condition: Is it True?
2
If True, execute loop body
3
Update condition variable (if needed)
4
Go back to step 1, repeat until False

06. ๐Ÿ“š Dictionaries & Sets

Dictionaries and sets are Python's key-value and unordered collection data structures, respectively. While they share some similarities with lists and tuples, they serve fundamentally different purposes and have unique performance characteristics.

Understanding Hash Tables

Both dictionaries and sets are implemented as hash tables in Python. This means:
1. Fast lookups: O(1) average time complexity for accessing elements
2. Unordered: Items don't maintain insertion order (though Python 3.7+ preserves insertion order as an implementation detail)
3. Unique keys/elements: Dictionaries have unique keys, sets have unique elements
4. Mutable: Both can be modified after creation
This hash-based implementation makes them extremely efficient for membership testing and key-value lookups.

Dictionary Operations and Methods

Dictionaries are Python's implementation of associative arrays or hash maps. They store data as key-value pairs:

  • Creating dictionaries: {}, dict(), dictionary comprehensions
  • Accessing values: dict[key], dict.get(key, default)
  • Adding/updating: dict[key] = value, dict.update(other_dict)
  • Removing: del dict[key], dict.pop(key), dict.popitem(), dict.clear()
  • Checking existence: key in dict, dict.keys(), dict.values(), dict.items()

Dictionary Comprehensions

Similar to list comprehensions, Python supports dictionary comprehensions for concise dictionary creation:
Traditional way:
squares = {}
for i in range(5):
    squares[i] = i**2


Dictionary comprehension way:
squares = {i: i**2 for i in range(5)}

With conditional logic:
even_squares = {i: i**2 for i in range(10) if i % 2 == 0}

Set Operations and Methods

Sets are unordered collections of unique elements, perfect for mathematical set operations:

  • Creating sets: {} (careful: empty set is set()), set(), set comprehensions
  • Adding elements: set.add(element), set.update(iterable)
  • Removing elements: set.remove(element), set.discard(element), set.pop()
  • Set operations: Union (|), intersection (&), difference (-), symmetric difference (^)
  • Membership testing: element in set (extremely fast O(1))

**Choosing Between Data Structures**: Use lists for ordered, mutable sequences; tuples for ordered, immutable sequences; dictionaries for key-value mappings; sets for unique element collections and mathematical operations.

Example: Comprehensive Dictionary and Set Operations

# ========== DICTIONARIES ==========
print("=== Dictionary Examples ===")

# Creating dictionaries
person = {
    "name": "Alice",
    "age": 30,
    "city": "New York",
    "email": "alice@example.com"
}

# Different ways to create dictionaries
empty_dict = {}
another_dict = dict(name="Bob", age=25)  # Using dict() constructor
coordinates = dict([("x", 10), ("y", 20), ("z", 30)])  # From list of tuples

print(f"Person dictionary: {person}")

# Accessing values
print(f"Name: {person['name']}")  # Direct access
print(f"Age: {person.get('age')}")  # Using get()
print(f"Country: {person.get('country', 'USA')}")  # get() with default

# Modifying dictionaries
person["age"] = 31  # Update existing key
person["country"] = "USA"  # Add new key-value pair
print(f"Updated person: {person}")

# Dictionary methods
print(f"\nKeys: {list(person.keys())}")
print(f"Values: {list(person.values())}")
print(f"Items: {list(person.items())}")

# Dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
print(f"\nSquares dictionary: {squares}")

# Filtering dictionaries with comprehension
student_scores = {"Alice": 85, "Bob": 92, "Charlie": 78, "David": 95}
top_students = {name: score for name, score in student_scores.items() if score >= 90}
print(f"Top students: {top_students}")

# Merging dictionaries (Python 3.9+)
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
merged = dict1 | dict2  # Union operator
print(f"\nMerged dictionaries: {merged}")

# ========== SETS ==========
print("\n=== Set Examples ===")

# Creating sets
fruits = {"apple", "banana", "cherry", "apple"}  # Duplicates removed
numbers = set([1, 2, 3, 4, 5, 2, 3])  # From list
empty_set = set()  # Note: {} creates empty dictionary!

print(f"Fruits set (duplicates removed): {fruits}")
print(f"Numbers set: {numbers}")

# Set operations
set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}

print(f"\nSet A: {set_a}")
print(f"Set B: {set_b}")
print(f"Union (A | B): {set_a | set_b}")
print(f"Intersection (A & B): {set_a & set_b}")
print(f"Difference (A - B): {set_a - set_b}")
print(f"Symmetric Difference (A ^ B): {set_a ^ set_b}")

# Set methods
set_a.add(6)  # Add single element
print(f"\nAfter add(6): {set_a}")

set_a.update({7, 8, 9})  # Add multiple elements
print(f"After update: {set_a}")

set_a.remove(9)  # Remove element (raises error if not found)
print(f"After remove(9): {set_a}")

set_a.discard(10)  # Remove if exists (no error if not found)
print(f"After discard(10): {set_a}")

# Set comprehension
even_squares = {x**2 for x in range(10) if x % 2 == 0}
print(f"\nEven squares set: {even_squares}")

# Practical use: Removing duplicates from list
data_with_duplicates = [1, 2, 2, 3, 4, 4, 4, 5, 5]
unique_data = list(set(data_with_duplicates))
print(f"\nOriginal list with duplicates: {data_with_duplicates}")
print(f"Unique elements: {unique_data}")

# ========== FROZENSET ==========
print("\n=== Frozenset Examples ===")

# Frozenset is immutable version of set
frozen = frozenset([1, 2, 3, 4, 5])
print(f"Frozenset: {frozen}")
print(f"Type: {type(frozen)}")

# Frozensets can be dictionary keys (regular sets cannot)
dict_with_frozenset = {
    frozenset([1, 2, 3]): "first set",
    frozenset([4, 5, 6]): "second set"
}
print(f"\nDictionary with frozenset keys: {dict_with_frozenset}")

# ========== PRACTICAL EXAMPLE ==========
print("\n=== Practical Example: Word Frequency Counter ===")

text = "apple banana apple cherry banana apple date"
words = text.split()

# Using dictionary for frequency counting
word_count = {}
for word in words:
    word_count[word] = word_count.get(word, 0) + 1

print(f"Text: '{text}'")
print(f"Word frequencies: {word_count}")

# Using set to find unique words
unique_words = set(words)
print(f"Unique words: {unique_words}")
print(f"Number of unique words: {len(unique_words)}")

When to Use Each Data Structure

Use dictionaries when:
1. You need to map keys to values (like a real dictionary)
2. You need fast lookups by key (O(1) time complexity)
3. Data has natural key-value relationships (username โ†’ user info)
4. You need to count frequencies or group items

Use sets when:
1. You need to ensure all elements are unique
2. You need fast membership testing (O(1) time complexity)
3. You need to perform mathematical set operations
4. You're removing duplicates from a collection

Data Structure Comparison:

๐Ÿ“– Dictionary

{"key": "value"}

Key-value pairs, fast lookups, mutable

๐Ÿ”ข Set

{1, 2, 3}

Unique elements, unordered, mutable

โ„๏ธ Frozenset

frozenset([1, 2])

Immutable set, hashable (can be dict key)

๐Ÿ’ก Performance Tip: Checking if key in dictionary or if element in set is O(1) average time, making them much faster than checking if element in list which is O(n).

07. โš™๏ธ Functions Deep Dive

Functions are reusable blocks of code that perform specific tasks. They are fundamental to writing clean, maintainable, and modular Python code. Functions help avoid repetition (DRY principle - Don't Repeat Yourself) and make your code more organized and testable.

Anatomy of a Python Function

A Python function definition consists of several parts:
1. def keyword to define the function
2. Function name (follows variable naming conventions)
3. Parentheses containing parameters (optional)
4. Colon to start the function body
5. Indented function body
6. return statement (optional) to return a value
Example: def greet(name): return f"Hello, {name}!"

Function Parameters and Arguments

Python provides several ways to pass arguments to functions:

  • Positional arguments: Matched by position in function call
  • Keyword arguments: Matched by parameter name
  • Default parameters: Provide default values if argument not supplied
  • *args: Collects extra positional arguments as tuple
  • **kwargs: Collects extra keyword arguments as dictionary

Variable Scope in Functions

Python has four levels of variable scope:
1. Local: Inside the current function
2. Enclosing: In enclosing functions (for nested functions)
3. Global: At the module level
4. Built-in: In the built-in namespace
The global keyword allows modifying global variables from within a function, and the nonlocal keyword allows modifying variables in enclosing (non-global) scope.

Lambda Functions and Functional Programming

Lambda functions (anonymous functions) are small, single-expression functions defined with the lambda keyword. They're useful for simple operations that can be expressed in a single line:

  • Syntax: lambda arguments: expression
  • Use with: map(), filter(), sorted()
  • Limitations: Only one expression, no statements, no annotations

**First-Class Functions**: In Python, functions are first-class objects, meaning they can be passed as arguments to other functions, returned as values from functions, and assigned to variables. This enables powerful programming patterns like decorators and higher-order functions.

Example: Comprehensive Function Examples

# ========== BASIC FUNCTION DEFINITION ==========
print("=== Basic Function Examples ===")

def greet(name):
    """Return a greeting message."""
    return f"Hello, {name}!"

result = greet("Alice")
print(f"greet('Alice'): {result}")

# Function with multiple parameters
def calculate_area(length, width):
    """Calculate the area of a rectangle."""
    return length * width

area = calculate_area(10, 5)
print(f"Area of 10x5 rectangle: {area}")

# ========== PARAMETER TYPES ==========
print("\n=== Parameter Types ===")

# Default parameters
def greet_person(name, greeting="Hello", punctuation="!"):
    """Greet a person with optional greeting and punctuation."""
    return f"{greeting}, {name}{punctuation}"

print(f"Default: {greet_person('Bob')}")
print(f"Custom greeting: {greet_person('Charlie', 'Hi')}")
print(f"Full custom: {greet_person('David', 'Hey', '!!!')}")

# *args for variable positional arguments
def sum_numbers(*args):
    """Sum any number of arguments."""
    print(f"args type: {type(args)}, args value: {args}")
    return sum(args)

print(f"\nSum of 1, 2, 3: {sum_numbers(1, 2, 3)}")
print(f"Sum of 1, 2, 3, 4, 5: {sum_numbers(1, 2, 3, 4, 5)}")

# **kwargs for variable keyword arguments
def print_info(**kwargs):
    """Print information from keyword arguments."""
    print(f"kwargs type: {type(kwargs)}")
    for key, value in kwargs.items():
        print(f"  {key}: {value}")

print("\nCalling print_info with kwargs:")
print_info(name="Alice", age=30, city="New York")

# Combining all parameter types
def comprehensive_function(positional, default="default", *args, **kwargs):
    """Demonstrate all parameter types."""
    print(f"Positional: {positional}")
    print(f"Default: {default}")
    print(f"*args: {args}")
    print(f"**kwargs: {kwargs}")

print("\nComprehensive function call:")
comprehensive_function("pos1", "custom", "arg1", "arg2", key1="val1", key2="val2")

# ========== RETURN VALUES ==========
print("\n=== Return Values ===")

# Multiple return values (as tuple)
def min_max(numbers):
    """Return both minimum and maximum of a list."""
    return min(numbers), max(numbers)

numbers = [5, 2, 8, 1, 9, 3]
minimum, maximum = min_max(numbers)
print(f"Numbers: {numbers}")
print(f"Min: {minimum}, Max: {maximum}")

# Returning different types based on conditions
def process_value(value):
    """Process a value and return appropriate type."""
    if isinstance(value, str):
        return value.upper()
    elif isinstance(value, (int, float)):
        return value * 2
    else:
        return None

print(f"\nProcess string: {process_value('hello')}")
print(f"Process number: {process_value(42)}")
print(f"Process list: {process_value([1, 2, 3])}")

# ========== VARIABLE SCOPE ==========
print("\n=== Variable Scope ===")

global_var = "I'm global"

def scope_demo():
    """Demonstrate variable scope."""
    local_var = "I'm local"
    print(f"Inside function - local_var: {local_var}")
    print(f"Inside function - global_var: {global_var}")
    
    # Modifying global variable
    global global_var
    global_var = "Modified global"
    
    # Nested function demonstrating nonlocal
    def nested():
        nonlocal local_var
        local_var = "Modified local"
    
    nested()
    print(f"After nested - local_var: {local_var}")

scope_demo()
print(f"Outside function - global_var: {global_var}")

# ========== LAMBDA FUNCTIONS ==========
print("\n=== Lambda Functions ===")

# Basic lambda
square = lambda x: x ** 2
print(f"Square of 5: {square(5)}")

# Lambda with multiple parameters
multiply = lambda x, y: x * y
print(f"Multiply 3 and 4: {multiply(3, 4)}")

# Using lambda with map()
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(f"Numbers: {numbers}")
print(f"Squared: {squared}")

# Using lambda with filter()
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(f"Even numbers: {evens}")

# Using lambda with sorted()
names = ["Alice", "Bob", "Charlie", "David"]
sorted_by_length = sorted(names, key=lambda x: len(x))
print(f"Names: {names}")
print(f"Sorted by length: {sorted_by_length}")

# ========== HIGHER-ORDER FUNCTIONS ==========
print("\n=== Higher-Order Functions ===")

# Function that returns a function
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)}")

# Function that accepts a function as argument
def apply_operation(numbers, operation):
    """Apply an operation to each number in a list."""
    return [operation(num) for num in numbers]

numbers = [1, 2, 3, 4, 5]
result = apply_operation(numbers, lambda x: x * 2)
print(f"Numbers: {numbers}")
print(f"After doubling: {result}")

# ========== PRACTICAL EXAMPLES ==========
print("\n=== Practical Function Examples ===")

# Calculator function with error handling
def calculator(operation, *args):
    """Perform basic arithmetic operations."""
    if not args:
        return 0
    
    if operation == "add":
        return sum(args)
    elif operation == "multiply":
        result = 1
        for num in args:
            result *= num
        return result
    elif operation == "average":
        return sum(args) / len(args)
    else:
        raise ValueError(f"Unknown operation: {operation}")

print(f"Add 1, 2, 3, 4: {calculator('add', 1, 2, 3, 4)}")
print(f"Multiply 2, 3, 4: {calculator('multiply', 2, 3, 4)}")
print(f"Average of 10, 20, 30: {calculator('average', 10, 20, 30)}")

# Data validation function
def validate_user(username, password, age):
    """Validate user registration data."""
    errors = []
    
    if len(username) < 3:
        errors.append("Username must be at least 3 characters")
    
    if len(password) < 8:
        errors.append("Password must be at least 8 characters")
    
    if age < 13:
        errors.append("Must be at least 13 years old")
    
    return errors

print("\nUser validation:")
errors = validate_user("ab", "short", 10)
if errors:
    print(f"Validation errors: {errors}")
else:
    print("Validation passed")

Best Practices for Writing Functions

1. Single responsibility: Each function should do one thing well
2. Descriptive names: Use verbs that describe what the function does
3. Keep them short: Functions should generally fit on one screen
4. Use docstrings: Document what the function does, its parameters, and return values
5. Limit side effects: Functions should preferably return values rather than modifying external state
6. Use type hints: Python 3.5+ supports type annotations for better code clarity
7. Default to immutability: Don't modify mutable arguments unless that's the function's purpose

Function Anatomy Visualization:

def function_name(parameter1, parameter2="default"):
    """Docstring: describes what the function does."""

    if condition:
        result = parameter1 + parameter2
    else:
        result = parameter1 * parameter2

    return result
def keyword Function name Parameters Docstring Function body Return

08. ๐Ÿ—๏ธ Object-Oriented Programming (OOP)

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. Python is a multi-paradigm language that fully supports OOP, allowing you to create clean, modular, and reusable code through classes and objects.

The Four Pillars of OOP

OOP in Python is built on four fundamental principles:
1. Encapsulation: Bundling data and methods that operate on that data within a single unit (class)
2. Abstraction: Hiding complex implementation details and exposing only essential features
3. Inheritance: Creating new classes based on existing classes
4. Polymorphism: Using a single interface for different underlying forms (data types)
These principles work together to create flexible, maintainable, and scalable code.

Classes and Objects

In Python, everything is an object. Classes are blueprints for creating objects (instances):

  • Class: A blueprint that defines attributes and methods
  • Object: An instance of a class with actual values
  • Attributes: Variables that belong to an object or class
  • Methods: Functions that belong to an object or class
  • Constructor: __init__() method that initializes new objects
  • self: Reference to the current instance (first parameter of instance methods)

Special Methods (Dunder Methods)

Python classes can define special methods (double underscore methods or "dunder" methods) that provide specific functionality:
- __init__: Object initialization (constructor)
- __str__: String representation for users
- __repr__: Official string representation for developers
- __len__: Length of the object
- __add__, __sub__, etc.: Operator overloading
- __getitem__, __setitem__: Indexing support
These methods allow your objects to integrate seamlessly with Python's built-in features.

Class and Instance Attributes

Python distinguishes between class attributes and instance attributes:

  • Class attributes: Shared by all instances of the class
  • Instance attributes: Unique to each instance
  • Property decorators: @property, @attribute.setter, @attribute.deleter
  • Class methods: @classmethod decorator, operate on the class itself
  • Static methods: @staticmethod decorator, don't access class or instance

**Encapsulation and Name Mangling**: Python uses name mangling (prefixing with _ClassName) for "private" attributes (starting with __). While Python doesn't have true private attributes, this convention indicates that they shouldn't be accessed directly from outside the class.

Example: Comprehensive OOP Examples

# ========== BASIC CLASS DEFINITION ==========
print("=== Basic Class Examples ===")

class Dog:
    """A simple Dog class."""
    
    # Class attribute (shared by all instances)
    species = "Canis familiaris"
    
    def __init__(self, name, age):
        """Initialize a new Dog instance."""
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age
    
    def bark(self):
        """Make the dog bark."""
        return f"{self.name} says Woof!"
    
    def describe(self):
        """Return a description of the dog."""
        return f"{self.name} is {self.age} years old."

# Creating instances
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

print(f"Dog1: {dog1.describe()}")
print(f"Dog2: {dog2.describe()}")
print(f"Dog1 bark: {dog1.bark()}")
print(f"Species (class attribute): {Dog.species}")

# ========== SPECIAL METHODS ==========
print("\n=== Special Methods (Dunder Methods) ===")

class Vector:
    """A simple 2D vector class."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        """String representation for users."""
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        """Official string representation for developers."""
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, other):
        """Vector addition."""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        """Vector subtraction."""
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        """Scalar multiplication."""
        return Vector(self.x * scalar, self.y * scalar)
    
    def __eq__(self, other):
        """Check equality."""
        return self.x == other.x and self.y == other.y
    
    def __len__(self):
        """Return magnitude (rounded)."""
        return int((self.x**2 + self.y**2)**0.5)
    
    def magnitude(self):
        """Calculate the exact magnitude."""
        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 * 2: {v1 * 2}")
print(f"Are v1 and v2 equal? {v1 == v2}")
print(f"Length of v1: {len(v1)}")
print(f"Magnitude of v1: {v1.magnitude()}")

# ========== PROPERTIES AND ENCAPSULATION ==========
print("\n=== Properties and Encapsulation ===")

class BankAccount:
    """A bank account class with encapsulation."""
    
    def __init__(self, owner, initial_balance=0):
        self.owner = owner
        self._balance = initial_balance  # Protected attribute
        self.__account_id = id(self)     # Private attribute (name mangled)
    
    @property
    def balance(self):
        """Get the current balance."""
        return self._balance
    
    @balance.setter
    def balance(self, amount):
        """Set balance with validation."""
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = amount
    
    def deposit(self, amount):
        """Deposit money into account."""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        return f"Deposited ${amount}. New balance: ${self._balance}"
    
    def withdraw(self, amount):
        """Withdraw money from account."""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        return f"Withdrew ${amount}. New balance: ${self._balance}"
    
    def get_account_id(self):
        """Public method to access private attribute."""
        return f"Account ID: {self.__account_id}"

account = BankAccount("Alice", 1000)
print(f"Account owner: {account.owner}")
print(f"Initial balance: ${account.balance}")
print(account.deposit(500))
print(account.withdraw(200))
print(f"Final balance: ${account.balance}")
print(account.get_account_id())

# Trying to access private attribute directly (will fail)
try:
    print(account.__account_id)
except AttributeError as e:
    print(f"Cannot access private attribute: {e}")

# ========== CLASS AND STATIC METHODS ==========
print("\n=== Class and Static Methods ===")

class TemperatureConverter:
    """A utility class for temperature conversions."""
    
    @staticmethod
    def celsius_to_fahrenheit(celsius):
        """Convert Celsius to Fahrenheit."""
        return (celsius * 9/5) + 32
    
    @staticmethod
    def fahrenheit_to_celsius(fahrenheit):
        """Convert Fahrenheit to Celsius."""
        return (fahrenheit - 32) * 5/9
    
    @classmethod
    def from_string(cls, temp_string):
        """Create converter from string like '25C' or '77F'."""
        value = float(temp_string[:-1])
        unit = temp_string[-1].upper()
        
        if unit == 'C':
            return {"celsius": value, "fahrenheit": cls.celsius_to_fahrenheit(value)}
        elif unit == 'F':
            return {"celsius": cls.fahrenheit_to_celsius(value), "fahrenheit": value}
        else:
            raise ValueError("Invalid temperature unit. Use 'C' or 'F'")

print(f"20ยฐC in Fahrenheit: {TemperatureConverter.celsius_to_fahrenheit(20)}")
print(f"68ยฐF in Celsius: {TemperatureConverter.fahrenheit_to_celsius(68)}")

temp_data = TemperatureConverter.from_string("25C")
print(f"From '25C': {temp_data}")

# ========== PRACTICAL EXAMPLE: E-COMMERCE ==========
print("\n=== Practical Example: E-commerce System ===")

class Product:
    """Represents a product in an e-commerce system."""
    
    def __init__(self, product_id, name, price, stock):
        self.product_id = product_id
        self.name = name
        self.price = price
        self.stock = stock
    
    def __str__(self):
        return f"{self.name} - ${self.price} ({self.stock} in stock)"
    
    def apply_discount(self, percentage):
        """Apply a discount to the product."""
        discount_amount = self.price * (percentage / 100)
        self.price -= discount_amount
        return f"Applied {percentage}% discount. New price: ${self.price:.2f}"
    
    def update_stock(self, quantity):
        """Update stock quantity."""
        if self.stock + quantity < 0:
            raise ValueError("Cannot have negative stock")
        self.stock += quantity
        return f"Stock updated by {quantity}. New stock: {self.stock}"

class ShoppingCart:
    """Represents a shopping cart."""
    
    def __init__(self):
        self.items = {}  # product_id: (product, quantity)
    
    def add_item(self, product, quantity=1):
        """Add an item to the cart."""
        if product.stock < quantity:
            raise ValueError(f"Insufficient stock for {product.name}")
        
        if product.product_id in self.items:
            existing_product, existing_quantity = self.items[product.product_id]
            self.items[product.product_id] = (product, existing_quantity + quantity)
        else:
            self.items[product.product_id] = (product, quantity)
        
        return f"Added {quantity} x {product.name} to cart"
    
    def remove_item(self, product_id, quantity=None):
        """Remove an item from the cart."""
        if product_id not in self.items:
            raise ValueError("Product not in cart")
        
        product, current_quantity = self.items[product_id]
        
        if quantity is None or quantity >= current_quantity:
            del self.items[product_id]
            return f"Removed all {product.name} from cart"
        else:
            self.items[product_id] = (product, current_quantity - quantity)
            return f"Removed {quantity} x {product.name} from cart"
    
    def calculate_total(self):
        """Calculate the total cost of items in cart."""
        total = 0
        for product, quantity in self.items.values():
            total += product.price * quantity
        return total
    
    def __str__(self):
        if not self.items:
            return "Shopping cart is empty"
        
        cart_contents = "Shopping Cart:\n"
        for product, quantity in self.items.values():
            cart_contents += f"  {quantity} x {product.name}: ${product.price * quantity:.2f}\n"
        cart_contents += f"Total: ${self.calculate_total():.2f}"
        return cart_contents

# Create products
laptop = Product("P001", "Laptop", 999.99, 10)
phone = Product("P002", "Smartphone", 599.99, 25)
headphones = Product("P003", "Wireless Headphones", 149.99, 50)

print(f"Products available:")
print(f"  1. {laptop}")
print(f"  2. {phone}")
print(f"  3. {headphones}")

# Create shopping cart
cart = ShoppingCart()
print("\n" + cart.add_item(laptop, 1))
print(cart.add_item(phone, 2))
print(cart.add_item(headphones, 1))

print("\n" + str(cart))

# Apply discount to a product
print("\n" + laptop.apply_discount(10))

print("\nUpdated cart after discount:")
print(cart)

Special Methods (Dunder Methods) Continued

Special methods allow Python objects to integrate with the language's syntax. They're called "dunder" methods because they're surrounded by double underscores. Some additional important dunder methods:
- __call__: Makes an instance callable like a function
- __iter__ and __next__: Makes an object iterable
- __enter__ and __exit__: For context managers (with statements)
- __getattr__ and __setattr__: Custom attribute access
- __contains__: For membership testing with 'in'

OOP Concepts Visualization:

๐Ÿ—๏ธ Class (Blueprint)

class Dog:
    species = "Canis familiaris"
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def bark(self):
        return f"{self.name} says Woof!"
โ†“ Instantiation

๐Ÿ• Object 1 (Instance)

dog1 = Dog("Buddy", 3)

Attributes:
- name: "Buddy"
- age: 3
- species: "Canis familiaris"

Methods:
- bark() โ†’ "Buddy says Woof!"

๐Ÿ• Object 2 (Instance)

dog2 = Dog("Max", 5)

Attributes:
- name: "Max"
- age: 5
- species: "Canis familiaris"

Methods:
- bark() โ†’ "Max says Woof!"

09. ๐Ÿงฌ Inheritance & Polymorphism

Inheritance and polymorphism are two of the four pillars of Object-Oriented Programming. Inheritance allows new classes to be based on existing classes, promoting code reuse. Polymorphism enables objects of different classes to be treated as objects of a common superclass, providing flexibility in design.

Types of Inheritance in Python

Python supports several inheritance patterns:
1. Single Inheritance: A class inherits from one parent class
2. Multiple Inheritance: A class inherits from multiple parent classes
3. Multilevel Inheritance: A class inherits from a parent, which itself inherits from another class
4. Hierarchical Inheritance: Multiple classes inherit from a single parent class
5. Hybrid Inheritance: A combination of two or more inheritance types
Python's Method Resolution Order (MRO) determines which method to call in complex inheritance hierarchies.

Method Overriding and Super()

When a child class defines a method with the same name as a parent class method, it overrides the parent method. The super() function allows you to call the parent class's methods:

  • Method overriding: Child class provides specific implementation
  • super() function: Accesses parent class methods and attributes
  • Constructor chaining: Using super().__init__() in child constructors
  • Diamond problem: Resolved by Python's C3 linearization algorithm

Abstract Base Classes (ABCs)

Python's abc module provides Abstract Base Classes for defining interfaces:
1. Use ABC as a base class
2. Mark methods as abstract with @abstractmethod
3. Cannot instantiate classes with abstract methods
4. Forces child classes to implement specific methods
ABCs are useful for defining common interfaces that multiple classes should implement.

Polymorphism in Action

Polymorphism allows objects of different classes to respond to the same method call in different ways:

  • Duck typing: "If it looks like a duck and quacks like a duck, it's a duck"
  • Operator overloading: Customizing behavior of operators (+, -, *, etc.)
  • Method polymorphism: Same method name, different implementations
  • Interface polymorphism: Different classes implementing the same interface

**Method Resolution Order (MRO)**: Python uses the C3 linearization algorithm to determine the order in which base classes are searched when looking for a method. You can view a class's MRO using ClassName.__mro__ or ClassName.mro().

Example: Comprehensive Inheritance & Polymorphism

# ========== SINGLE INHERITANCE ==========
print("=== Single Inheritance ===")

class Animal:
    """Base class for all animals."""
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def speak(self):
        """Make animal sound."""
        raise NotImplementedError("Subclasses must implement speak()")
    
    def describe(self):
        return f"{self.name} is {self.age} years old."

class Dog(Animal):
    """Dog class inherits from Animal."""
    
    def __init__(self, name, age, breed):
        # Call parent class constructor
        super().__init__(name, age)
        self.breed = breed
    
    # Override the speak method
    def speak(self):
        return f"{self.name} says Woof!"
    
    # Add new method specific to Dog
    def fetch(self, item):
        return f"{self.name} fetches the {item}."

class Cat(Animal):
    """Cat class inherits from Animal."""
    
    def __init__(self, name, age, color):
        super().__init__(name, age)
        self.color = color
    
    # Override the speak method differently
    def speak(self):
        return f"{self.name} says Meow!"
    
    # Add new method specific to Cat
    def purr(self):
        return f"{self.name} purrs softly."

# Create instances
dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2, "Gray")

print(f"Dog: {dog.describe()}")
print(f"Dog speak: {dog.speak()}")
print(f"Dog fetch: {dog.fetch('ball')}")

print(f"\nCat: {cat.describe()}")
print(f"Cat speak: {cat.speak()}")
print(f"Cat purr: {cat.purr()}")

# ========== POLYMORPHISM DEMONSTRATION ==========
print("\n=== Polymorphism Example ===")

def animal_sounds(animals):
    """Demonstrate polymorphism - works with any Animal."""
    for animal in animals:
        print(f"{animal.name}: {animal.speak()}")

# Create a list of different animals
animals = [
    Dog("Rex", 4, "German Shepherd"),
    Cat("Mittens", 1, "White"),
    Dog("Luna", 2, "Labrador")
]

print("Animal sounds:")
animal_sounds(animals)

# ========== MULTIPLE INHERITANCE ==========
print("\n=== Multiple Inheritance ===")

class Flyable:
    """Mixin class for flying ability."""
    
    def fly(self):
        return f"{self.name} is flying!"
    
    def altitude(self):
        return "High in the sky"

class Swimmable:
    """Mixin class for swimming ability."""
    
    def swim(self):
        return f"{self.name} is swimming!"
    
    def depth(self):
        return "Deep in the water"

class Duck(Animal, Flyable, Swimmable):
    """Duck inherits from Animal, Flyable, and Swimmable."""
    
    def __init__(self, name, age):
        super().__init__(name, age)
    
    def speak(self):
        return f"{self.name} says Quack!"

duck = Duck("Donald", 1)
print(f"Duck: {duck.describe()}")
print(f"Duck speak: {duck.speak()}")
print(f"Duck fly: {duck.fly()}")
print(f"Duck swim: {duck.swim()}")

# Check Method Resolution Order
print(f"\nDuck MRO: {[cls.__name__ for cls in Duck.__mro__]}")

# ========== ABSTRACT BASE CLASSES ==========
print("\n=== Abstract Base Classes ===")

from abc import ABC, abstractmethod

class Shape(ABC):
    """Abstract base class for shapes."""
    
    @abstractmethod
    def area(self):
        """Calculate area of shape."""
        pass
    
    @abstractmethod
    def perimeter(self):
        """Calculate perimeter of shape."""
        pass
    
    def describe(self):
        """Concrete method available to all shapes."""
        return f"This shape has area {self.area()} and perimeter {self.perimeter()}"

class Rectangle(Shape):
    """Rectangle implements Shape interface."""
    
    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):
    """Circle implements Shape interface."""
    
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Create shapes
shapes = [
    Rectangle(5, 3),
    Circle(4),
    Rectangle(2, 2)
]

print("Shape calculations:")
for shape in shapes:
    print(f"  {shape.describe()}")

# ========== METHOD OVERRIDING WITH SUPER() ==========
print("\n=== Method Overriding with super() ===")

class Vehicle:
    """Base vehicle class."""
    
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def start(self):
        return "Starting vehicle..."
    
    def stop(self):
        return "Stopping vehicle..."

class ElectricCar(Vehicle):
    """Electric car with additional features."""
    
    def __init__(self, brand, model, battery_capacity):
        super().__init__(brand, model)
        self.battery_capacity = battery_capacity
        self.is_charging = False
    
    # Override start method with additional functionality
    def start(self):
        base_start = super().start()  # Get parent's start message
        return f"{base_start} Electric motor engaged. Battery: {self.battery_capacity}kWh"
    
    # Add new method
    def charge(self):
        self.is_charging = True
        return f"{self.brand} {self.model} is charging"

car = ElectricCar("Tesla", "Model 3", 75)
print(f"Car start: {car.start()}")
print(f"Car charge: {car.charge()}")
print(f"Car stop: {car.stop()}")

# ========== PROPERTY OVERRIDING ==========
print("\n=== Property Overriding ===")

class Person:
    """Base person class."""
    
    def __init__(self, name):
        self._name = name
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        print(f"Changing name from {self._name} to {value}")
        self._name = value

class Employee(Person):
    """Employee inherits from Person."""
    
    def __init__(self, name, employee_id):
        super().__init__(name)
        self.employee_id = employee_id
    
    @property
    def name(self):
        # Add employee ID to name
        return f"{self._name} (ID: {self.employee_id})"
    
    @name.setter
    def name(self, value):
        # Custom setter with validation
        if len(value) < 2:
            raise ValueError("Name must be at least 2 characters")
        super(Employee, type(self)).name.fset(self, value)

emp = Employee("John Doe", "E12345")
print(f"Employee name: {emp.name}")
emp.name = "Jane Smith"
print(f"Updated name: {emp.name}")

# ========== PRACTICAL EXAMPLE: PAYMENT SYSTEM ==========
print("\n=== Practical Example: Payment System ===")

class PaymentMethod(ABC):
    """Abstract base class for payment methods."""
    
    @abstractmethod
    def process_payment(self, amount):
        """Process a payment of given amount."""
        pass
    
    @abstractmethod
    def get_details(self):
        """Get payment method details."""
        pass

class CreditCard(PaymentMethod):
    """Credit card payment method."""
    
    def __init__(self, card_number, expiry_date, cvv):
        self.card_number = card_number
        self.expiry_date = expiry_date
        self.cvv = cvv
    
    def process_payment(self, amount):
        # Simulate credit card processing
        return f"Processing ${amount:.2f} via Credit Card ending in {self.card_number[-4:]}"
    
    def get_details(self):
        return f"Credit Card: **** **** **** {self.card_number[-4:]}"

class PayPal(PaymentMethod):
    """PayPal payment method."""
    
    def __init__(self, email):
        self.email = email
    
    def process_payment(self, amount):
        # Simulate PayPal processing
        return f"Processing ${amount:.2f} via PayPal ({self.email})"
    
    def get_details(self):
        return f"PayPal: {self.email}"

class BankTransfer(PaymentMethod):
    """Bank transfer payment method."""
    
    def __init__(self, account_number, routing_number):
        self.account_number = account_number
        self.routing_number = routing_number
    
    def process_payment(self, amount):
        # Simulate bank transfer
        return f"Processing ${amount:.2f} via Bank Transfer to account ending in {self.account_number[-4:]}"
    
    def get_details(self):
        return f"Bank Transfer: Account ****{self.account_number[-4:]}"

class PaymentProcessor:
    """Processes payments using different payment methods."""
    
    def __init__(self):
        self.payment_methods = []
    
    def add_payment_method(self, payment_method):
        """Add a payment method to the processor."""
        self.payment_methods.append(payment_method)
    
    def process_all_payments(self, amount):
        """Process payments with all available methods (polymorphism)."""
        results = []
        for method in self.payment_methods:
            results.append(method.process_payment(amount))
        return results
    
    def list_payment_methods(self):
        """List all available payment methods."""
        return [method.get_details() for method in self.payment_methods]

# Create payment methods
credit_card = CreditCard("4111111111111111", "12/25", "123")
paypal = PayPal("user@example.com")
bank_transfer = BankTransfer("1234567890", "021000021")

# Create payment processor
processor = PaymentProcessor()
processor.add_payment_method(credit_card)
processor.add_payment_method(paypal)
processor.add_payment_method(bank_transfer)

print("Available payment methods:")
for method in processor.list_payment_methods():
    print(f"  - {method}")

print("\nProcessing $100 with all methods:")
results = processor.process_all_payments(100)
for result in results:
    print(f"  - {result}")

Composition vs Inheritance

While inheritance creates "is-a" relationships, composition creates "has-a" relationships. Composition is often preferred over inheritance because it provides more flexibility:

Inheritance: class Car(Vehicle): (A Car IS A Vehicle)
Composition: class Car: def __init__(self): self.engine = Engine() (A Car HAS AN Engine)

The "favor composition over inheritance" principle suggests using composition when possible, as it creates looser coupling and more flexible designs.

Inheritance Hierarchy Visualization:
Animal
Base class
โ†“ extends
Dog
speak(): "Woof!"
โ†“ extends
Cat
speak(): "Meow!"
โ†“ extends
Duck
speak(): "Quack!"
Flyable
(Mixin)
Swimmable
(Mixin)
Multiple Inheritance: Duck โ† Animal + Flyable + Swimmable

10. ๐Ÿ’พ File Handling (I/O)

File handling is a crucial aspect of programming that allows your applications to persist data between executions, read configuration files, process data from external sources, and much more. Python provides a comprehensive set of tools for working with files, making it easy to read from and write to both text and binary files.

Understanding File Modes

When opening files in Python, you specify a mode that determines how the file will be used:
- 'r': Read (default) - Opens file for reading
- 'w': Write - Opens file for writing (creates new or truncates existing)
- 'a': Append - Opens file for appending (creates if doesn't exist)
- 'x': Exclusive creation - Creates file, fails if exists
- 'b': Binary mode - Used with other modes (e.g., 'rb', 'wb')
- 't': Text mode (default) - Used with other modes (e.g., 'rt', 'wt')
- '+': Read and write - Updates mode (e.g., 'r+', 'w+')

File Operations and Methods

Python's file objects provide various methods for reading and writing data:

  • Reading methods: read(), readline(), readlines()
  • Writing methods: write(), writelines()
  • File positioning: seek(), tell()
  • File metadata: name, mode, closed
  • OS operations: os.rename(), os.remove(), os.path functions

Context Managers (The 'with' Statement)

The with statement is Python's recommended way to handle files because it automatically manages resources:
1. Opens the file
2. Executes the code block
3. Automatically closes the file, even if an error occurs
This prevents resource leaks and ensures files are properly closed. Always use with when working with files!

Working with Different File Formats

Python's standard library includes modules for working with various file formats:

  • CSV files: csv module for comma-separated values
  • JSON files: json module for JavaScript Object Notation
  • Configuration files: configparser for INI-style configs
  • Pickle: pickle module for Python object serialization
  • XML files: xml.etree.ElementTree for XML parsing
  • Binary files: Use 'b' mode for images, audio, etc.

**Pathlib Module**: Python 3.4+ introduced the pathlib module, which provides an object-oriented approach to file system paths. It's more intuitive and platform-independent than the older os.path module.

Example: Comprehensive File Handling Examples

# ========== BASIC FILE OPERATIONS ==========
print("=== Basic File Operations ===")

import os

# Create a sample text file
sample_content = """Python File Handling Examples
=======================

This file demonstrates various file operations:
1. Reading files
2. Writing files  
3. Appending to files
4. Working with file paths

Let's learn file handling in Python!
"""

# Writing to a file
print("Writing to file...")
with open("example.txt", "w", encoding="utf-8") as file:
    file.write(sample_content)
print("File 'example.txt' created successfully!")

# Reading entire file
print("\nReading entire file:")
with open("example.txt", "r", encoding="utf-8") as file:
    content = file.read()
    print(content)

# Reading line by line
print("Reading line by line:")
with open("example.txt", "r", encoding="utf-8") as file:
    for i, line in enumerate(file, 1):
        print(f"Line {i}: {line.rstrip()}")

# Reading specific number of characters
print("\nReading first 50 characters:")
with open("example.txt", "r", encoding="utf-8") as file:
    first_50 = file.read(50)
    print(f"First 50 chars: {first_50}")

# ========== FILE POSITIONING ==========
print("\n=== File Positioning ===")

with open("example.txt", "r+", encoding="utf-8") as file:
    # Get current position
    position = file.tell()
    print(f"Initial position: {position}")
    
    # Read first 20 characters
    first_part = file.read(20)
    print(f"First 20 chars: {first_part}")
    
    # Get new position
    position = file.tell()
    print(f"Position after reading 20 chars: {position}")
    
    # Move to beginning
    file.seek(0)
    position = file.tell()
    print(f"Position after seek(0): {position}")
    
    # Move to specific position
    file.seek(30)
    position = file.tell()
    print(f"Position after seek(30): {position}")
    
    # Read from current position
    from_30 = file.read(20)
    print(f"20 chars from position 30: {from_30}")

# ========== APPENDING TO FILES ==========
print("\n=== Appending to Files ===")

# Append to existing file
additional_content = "\n\n=== Appended Content ===\nThis content was added later."

with open("example.txt", "a", encoding="utf-8") as file:
    file.write(additional_content)

print("Content appended to file!")
print("\nUpdated file content:")
with open("example.txt", "r", encoding="utf-8") as file:
    print(file.read())

# ========== WORKING WITH CSV FILES ==========
print("\n=== Working with CSV Files ===")

import csv

# Create CSV data
employees = [
    ["Name", "Department", "Salary", "Join Date"],
    ["Alice Johnson", "Engineering", "85000", "2020-03-15"],
    ["Bob Smith", "Marketing", "72000", "2019-07-22"],
    ["Charlie Brown", "Sales", "68000", "2021-01-10"],
    ["Diana Prince", "HR", "65000", "2018-11-05"]
]

# Write CSV file
print("Creating employees.csv...")
with open("employees.csv", "w", newline="", encoding="utf-8") as csvfile:
    writer = csv.writer(csvfile)
    writer.writerows(employees)
print("CSV file created successfully!")

# Read CSV file
print("\nReading CSV file:")
with open("employees.csv", "r", encoding="utf-8") as csvfile:
    reader = csv.reader(csvfile)
    for row in reader:
        print(f"Row: {row}")

# Read CSV as dictionary
print("\nReading CSV as dictionary:")
with open("employees.csv", "r", encoding="utf-8") as csvfile:
    reader = csv.DictReader(csvfile)
    for record in reader:
        print(f"Record: {dict(record)}")

# ========== WORKING WITH JSON FILES ==========
print("\n=== Working with JSON Files ===")

import json

# Create Python dictionary
user_data = {
    "users": [
        {
            "id": 1,
            "name": "Alice",
            "email": "alice@example.com",
            "roles": ["admin", "user"],
            "active": True,
            "preferences": {
                "theme": "dark",
                "notifications": True
            }
        },
        {
            "id": 2,
            "name": "Bob",
            "email": "bob@example.com",
            "roles": ["user"],
            "active": True,
            "preferences": {
                "theme": "light",
                "notifications": False
            }
        },
        {
            "id": 3,
            "name": "Charlie",
            "email": "charlie@example.com",
            "roles": ["moderator", "user"],
            "active": False,
            "preferences": {
                "theme": "dark",
                "notifications": True
            }
        }
    ],
    "metadata": {
        "total_users": 3,
        "active_users": 2,
        "creation_date": "2023-10-01"
    }
}

# Write JSON file
print("Creating users.json...")
with open("users.json", "w", encoding="utf-8") as jsonfile:
    json.dump(user_data, jsonfile, indent=2)
print("JSON file created successfully!")

# Read JSON file
print("\nReading JSON file:")
with open("users.json", "r", encoding="utf-8") as jsonfile:
    loaded_data = json.load(jsonfile)
    print(f"Total users: {loaded_data['metadata']['total_users']}")
    print(f"Active users: {loaded_data['metadata']['active_users']}")
    print("\nUser details:")
    for user in loaded_data["users"]:
        print(f"  - {user['name']} ({user['email']}) - Roles: {', '.join(user['roles'])}")

# ========== BINARY FILES ==========
print("\n=== Working with Binary Files ===")

# Create and read binary file
binary_data = bytes(range(256))  # Create bytes from 0 to 255

print("Creating binary file...")
with open("data.bin", "wb") as binary_file:
    binary_file.write(binary_data)
print("Binary file created!")

print("\nReading binary file:")
with open("data.bin", "rb") as binary_file:
    # Read first 20 bytes
    first_bytes = binary_file.read(20)
    print(f"First 20 bytes: {list(first_bytes)}")
    
    # Jump to position 100
    binary_file.seek(100)
    bytes_at_100 = binary_file.read(10)
    print(f"10 bytes starting at position 100: {list(bytes_at_100)}")

# ========== FILE MANAGEMENT ==========
print("\n=== File Management Operations ===")

# Check if file exists
import os.path

files_to_check = ["example.txt", "employees.csv", "users.json", "data.bin", "nonexistent.txt"]

print("Checking file existence:")
for filename in files_to_check:
    exists = os.path.exists(filename)
    print(f"  {filename}: {'Exists' if exists else 'Does not exist'}")

# Get file information
print("\nFile information for 'example.txt':")
if os.path.exists("example.txt"):
    file_stats = os.stat("example.txt")
    print(f"  Size: {file_stats.st_size} bytes")
    print(f"  Last modified: {file_stats.st_mtime}")
    print(f"  Is file: {os.path.isfile('example.txt')}")
    print(f"  Is directory: {os.path.isdir('example.txt')}")

# List files in current directory
print("\nFiles in current directory:")
for item in os.listdir("."):
    if os.path.isfile(item):
        size = os.path.getsize(item)
        print(f"  {item} ({size} bytes)")

# ========== PATHLIB MODULE ==========
print("\n=== Using pathlib Module ===")

from pathlib import Path

# Create Path object
current_dir = Path(".")
example_file = Path("example.txt")

print(f"Current directory: {current_dir.absolute()}")
print(f"Example file exists: {example_file.exists()}")
print(f"Example file name: {example_file.name}")
print(f"Example file stem: {example_file.stem}")
print(f"Example file suffix: {example_file.suffix}")
print(f"Example file parent: {example_file.parent}")

# Create new directory
new_dir = Path("test_directory")
new_dir.mkdir(exist_ok=True)
print(f"\nCreated directory: {new_dir}")

# Create file in new directory
new_file = new_dir / "test.txt"
new_file.write_text("This is a test file created with pathlib!")
print(f"Created file: {new_file}")

# Read the file
content = new_file.read_text()
print(f"File content: {content}")

# ========== PRACTICAL EXAMPLE: LOGGING SYSTEM ==========
print("\n=== Practical Example: Logging System ===")

import datetime

class Logger:
    """A simple file-based logging system."""
    
    def __init__(self, log_file="app.log"):
        self.log_file = Path(log_file)
    
    def log(self, level, message):
        """Log a message with timestamp and level."""
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_entry = f"[{timestamp}] [{level.upper()}] {message}\n"
        
        with open(self.log_file, "a", encoding="utf-8") as file:
            file.write(log_entry)
        
        return log_entry.strip()
    
    def info(self, message):
        """Log an info message."""
        return self.log("INFO", message)
    
    def warning(self, message):
        """Log a warning message."""
        return self.log("WARNING", message)
    
    def error(self, message):
        """Log an error message."""
        return self.log("ERROR", message)
    
    def read_logs(self, level=None, limit=None):
        """Read logs, optionally filtered by level."""
        if not self.log_file.exists():
            return []
        
        logs = []
        with open(self.log_file, "r", encoding="utf-8") as file:
            for line in file:
                if level and f"[{level.upper()}]" not in line:
                    continue
                logs.append(line.strip())
        
        if limit:
            logs = logs[-limit:]  # Get last 'limit' entries
        
        return logs
    
    def clear_logs(self):
        """Clear all logs."""
        if self.log_file.exists():
            self.log_file.write_text("")
            return "Logs cleared"
        return "No logs to clear"

# Create logger instance
logger = Logger("application.log")

print("Generating log entries...")
logger.info("Application started")
logger.info("User 'alice' logged in")
logger.warning("Disk space is running low (85% used)")
logger.info("Processing data file 'data.csv'")
logger.error("Failed to connect to database: Connection timeout")
logger.info("User 'alice' logged out")

print("\nRecent logs:")
logs = logger.read_logs(limit=5)
for log in logs:
    print(f"  {log}")

print("\nError logs only:")
error_logs = logger.read_logs(level="error")
for log in error_logs:
    print(f"  {log}")

# ========== CLEANUP ==========
print("\n=== Cleanup ===")

# Remove created files
files_to_remove = [
    "example.txt",
    "employees.csv", 
    "users.json",
    "data.bin",
    "application.log",
    new_file,
    new_dir / "test.txt"
]

print("Cleaning up created files...")
for file_path in files_to_remove:
    path = Path(file_path)
    if path.exists():
        path.unlink()
        print(f"  Removed: {file_path}")

# Remove directory if empty
if new_dir.exists() and not any(new_dir.iterdir()):
    new_dir.rmdir()
    print(f"  Removed directory: {new_dir}")

print("\nAll file operations completed successfully!")

Best Practices for File Handling

1. Always use context managers: Use with statements to ensure files are properly closed
2. Handle exceptions: Use try-except blocks to handle file-related errors
3. Specify encoding: Always specify encoding (usually utf-8) for text files
4. Use appropriate modes: Choose the right file mode for your operation
5. Check file existence: Check if files exist before operations to avoid errors
6. Handle large files efficiently: For large files, read/write in chunks or line by line
7. Use platform-independent paths: Use pathlib for cross-platform compatibility
8. Clean up temporary files: Remove temporary files when done

File I/O Flow Visualization:
File Operations Workflow
1
Open File
with open("file.txt", "r") as file:
Context manager ensures proper cleanup
2
Read/Write Operations
Reading: file.read(), file.readlines()
Writing: file.write(), file.writelines()
3
File Positioning (Optional)
file.seek(0) - Move to beginning
file.tell() - Get current position
4
Automatic Cleanup
File automatically closed when exiting with block
Even if errors occur during operations
Common File Formats
๐Ÿ“„ Text
.txt, .py, .html
open("file.txt", "r")
๐Ÿ“Š CSV
.csv files
import csv
๐Ÿ”ฃ JSON
.json files
import json
๐Ÿ’พ Binary
.bin, .jpg, .mp3
open("file.bin", "rb")