Python basic to advance
43 subscribers
131 photos
98 links
Learn Python daily with bite-sized lessons, code examples, output previews, and fun quizzes. Perfect for beginners and enthusiasts!
Download Telegram
๐Ÿš€ Day 50: **The Genesis Engine: Architecting Code Mutations with Python's Abstract Syntax Tree (AST)!** ๐Ÿคฏ

Today, we go beyond writing code to *rewriting* it programmatically! The Abstract Syntax Tree (AST) allows you to dissect your Python code into its fundamental building blocks, inspect its structure, and even dynamically transform it. Imagine building custom linters, generating code, or even implementing domain-specific languages โ€“ all by manipulating the very fabric of Python itself! ๐Ÿ› ๏ธ Get ready to delve deep into the compiler's mind.

```python
import ast
import inspect

# Original function we want to "mutate"
def simple_math(x, y):
result = x + y * 2
return result

# 1. Get the AST of the function
source_code = inspect.getsource(simple_math)
tree = ast.parse(source_code)

print("Original AST (simplified view):")
print(ast.dump(tree, indent=4))

# 2. Define an AST NodeTransformer to modify the tree
class MultiplyToAddTransformer(ast.NodeTransformer):
"""
Transforms multiplication operations (*) into addition operations (+).
"""
def visit_BinOp(self, node):
# Check if the operation is multiplication
if isinstance(node.op, ast.Mult):
print(f"nโšก Transforming multiplication at line {node.lineno} to addition...")
node.op = ast.Add() # Change the operator from Mult to Add

# Ensure we visit child nodes as well
return self.generic_visit(node)

# 3. Apply the transformation
transformer = MultiplyToAddTransformer()
transformed_tree = transformer.visit(tree)

# ast.fix_missing_locations is important if you create new nodes
# or modify line/column numbers, ensuring the AST is valid for compilation.
ast.fix_missing_locations(transformed_tree)

print("nTransformed AST (simplified view):")
print(ast.dump(transformed_tree, indent=4))

# 4. Compile and execute the modified AST
# Compile the entire module represented by the transformed AST
compiled_code = compile(transformed_tree, '<string>', 'exec')

# Create a new namespace to execute the compiled code
mutated_namespace = {}
exec(compiled_code, mutated_namespace)

# The transformed function will now be in our mutated_namespace
mutated_simple_math = mutated_namespace['simple_math']

print("n--- Execution Results ---")
print(f"Original function 'simple_math(5, 3)': {simple_math(5, 3)}") # Expected: 5 + (3 * 2) = 11
print(f"Mutated function 'mutated_simple_math(5, 3)': {mutated_simple_math(5, 3)}") # Expected: 5 + (3 + 2) = 10 (because * became +)
```

Output example:
```
Original AST (simplified view):
Module(body=[FunctionDef(name='simple_math', args=arguments(posonlyargs=[], args=[arg(arg='x'), arg(arg='y')], kwonlyargs=[], kw_defaults=[], defaults=[]), body=[Assign(targets=[Name(id='result', ctx=Store())], value=BinOp(left=Name(id='x', ctx=Load()), op=Add(), right=BinOp(left=Name(id='y', ctx=Load()), op=Mult(), right=Constant(value=2)))), Return(value=Name(id='result', ctx=Load()))], decorator_list=[])], type_ignores=[])

โšก Transforming multiplication at line 3 to addition...

Transformed AST (simplified view):
Module(body=[FunctionDef(name='simple_math', args=arguments(posonlyargs=[], args=[arg(arg='x'), arg(arg='y')], kwonlyargs=[], kw_defaults=[], defaults=[]), body=[Assign(targets=[Name(id='result', ctx=Store())], value=BinOp(left=Name(id='x', ctx=Load()), op=Add(), right=BinOp(left=Name(id='y', ctx=Load()), op=Add(), right=Constant(value=2)))), Return(value=Name(id='result', ctx=Load()))], decorator_list=[])], type_ignores=[])

--- Execution Results ---
Original function 'simple_math(5, 3)': 11
Mutated function 'mutated_simple_math(5, 3)': 10
```

๐Ÿ’ก **Your Challenge:** Modify the `MultiplyToAddTransformer` to instead identify all `if` statements and wrap their bodies in a `try...except` block, catching a generic `Exception` and printing an error message. This is much harder but a fantastic dive into AST node creation!
๐Ÿš€ Day 51: Beyond the surface! Ever wondered how linters, formatters, or even Python itself understands your code? Enter the Abstract Syntax Tree (AST)! ๐ŸŒณ This powerful module lets you represent Python code as a tree structure, opening doors to programmatic analysis, modification, and generation of code.

It's like having X-ray vision for your scripts, allowing you to *programmatically reshape your Python code*. Ready to perform some code metamorphosis? Let's dive into the `ast` module and unlock its power! ๐Ÿ’ก

```python
import ast
import inspect

# Original Python code snippet
original_code = """
def greet(name):
print(f"Hello, {name}!")
return f"Welcome, {name}!"

class MyClass:
def __init__(self, value):
self.value = value

def display(self):
return f"Value: {self.value}"
"""

print("--- Original Code ---")
print(original_code)

# 1. Parse the code into an AST
tree = ast.parse(original_code)

# 2. Traverse and identify nodes (e.g., find function definitions)
print("n--- Identified Function Definitions ---")
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(f"Function found: '{node.name}' at line {node.lineno}")

# 3. Modify the AST: Add a print statement to the `greet` function
class FunctionModifier(ast.NodeTransformer):
"""
A transformer that adds a print statement to the beginning of the 'greet' function.
"""
def visit_FunctionDef(self, node):
if node.name == "greet":
# Create a new AST node for print("AST modified the greet function!")
new_print_call = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[ast.Constant(value='AST modified the greet function!')],
keywords=[]
)
)
# Add the new print statement to the beginning of the function body
node.body.insert(0, new_print_call)
return self.generic_visit(node) # Crucial: ensures child nodes are also visited

modified_tree = FunctionModifier().visit(tree)

# 4. Unparse the modified AST back to source code (requires Python 3.9+)
modified_code = ast.unparse(modified_tree)

print("n--- Modified Code ---")
print(modified_code)

# 5. Verify the modified code's behavior
print("n--- Modified Code Execution Example ---")
modified_exec_globals = {}
exec(modified_code, modified_exec_globals)
# Call the modified 'greet' function
print(modified_exec_globals['greet']("Bob"))
```

Output example:
```
--- Original Code ---

def greet(name):
print(f"Hello, {name}!")
return f"Welcome, {name}!"

class MyClass:
def __init__(self, value):
self.value = value

def display(self):
return f"Value: {self.value}"

--- Identified Function Definitions ---
Function found: 'greet' at line 2
Function found: '__init__' at line 8
Function found: 'display' at line 11

--- Modified Code ---

def greet(name):
print('AST modified the greet function!')
print(f"Hello, {name}!")
return f"Welcome, {name}!"

class MyClass:
def __init__(self, value):
self.value = value

def display(self):
return f"Value: {self.value}"

--- Modified Code Execution Example ---
AST modified the greet function!
Hello, Bob!
Welcome, Bob!
```

---

### Your Challenge! ๐Ÿง‘โ€๐Ÿ’ป
Using the `ast` module and an `ast.NodeTransformer`, write a simple script that takes a Python code string and replaces all occurrences of the string literal `"secret_data"` with `"redacted"`. You'll need to look for `ast.Constant` nodes (or `ast.Str` for older Python versions) and check their `value` attribute. Good luck! ๐Ÿ•ต๏ธโ€โ™‚๏ธ

---
๐Ÿš€ Day 53: Quantum Types! ๐Ÿคฏ Ever felt limited by Python's generics when dealing with arbitrary numbers of types or complex function signatures? Dive into the cutting-edge of Python's type system with `TypeVarTuple` and `ParamSpec`! โœจ

These powerful tools unlock "hyper-generics," allowing you to write incredibly flexible and type-safe code for functions that take variable arguments or need to preserve complex callable signatures. Get ready to stretch the boundaries of what you thought possible with type hints! ๐Ÿ’ก

```python
from typing import TypeVar, TypeVarTuple, ParamSpec, Callable, reveal_type
import time

# --- TypeVarTuple Example ---
# Ts represents an arbitrary number of types
Ts = TypeVarTuple('Ts')

def process_multiple_args(*args: *Ts) -> tuple[*Ts]:
"""Processes an arbitrary number of arguments, preserving their types."""
print(f"Received arguments: {args}")
return args

# --- ParamSpec Example ---
# P represents the parameter specification (args and kwargs) of a callable
P = ParamSpec('P')
R = TypeVar('R') # R represents the return type of the callable

def log_and_call_timed(func: Callable[P, R]) -> Callable[P, R]:
"""
A decorator that logs calls, measures execution time, and preserves
the decorated function's signature using ParamSpec.
"""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"โฑ๏ธ Calling '{func.__name__}' with args={args}, kwargs={kwargs}")
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"โœ… '{func.__name__}' returned: {result} (took {end_time - start_time:.4f}s)")
return result
return wrapper

@log_and_call_timed
def add_numbers(a: int, b: int) -> int:
time.sleep(0.01) # Simulate some work
return a + b

@log_and_call_timed
def greet(name: str, *, greeting: str = "Hello") -> str:
time.sleep(0.005) # Simulate some work
return f"{greeting}, {name}!"

# Demonstration
print("n--- TypeVarTuple Demonstration ---")
result_tuple = process_multiple_args(1, "hello", True, 3.14)
# reveal_type(result_tuple) # Uncomment to see type checker's insight

print("n--- ParamSpec Demonstration ---")
sum_result = add_numbers(5, 3)
greet_result = greet("Alice", greeting="Hi there")
# reveal_type(add_numbers) # Uncomment to see type checker's insight on the decorated function
# reveal_type(greet)
```

Output example:
```
--- TypeVarTuple Demonstration ---
Received arguments: (1, 'hello', True, 3.14)
Note: If you uncomment `reveal_type(result_tuple)` and run a type checker like mypy, it might show:
Revealed type is "tuple[int, str, bool, float]"

--- ParamSpec Demonstration ---
โฑ๏ธ Calling 'add_numbers' with args=(5, 3), kwargs={}
โœ… 'add_numbers' returned: 8 (took 0.01XXs)
โฑ๏ธ Calling 'greet' with args=('Alice',), kwargs={'greeting': 'Hi there'}
โœ… 'greet' returned: Hi there, Alice! (took 0.00XXs)
Note: If you uncomment `reveal_type(add_numbers)` and `reveal_type(greet)`:
Revealed type is "def (a: builtins.int, b: builtins.int) -> builtins.int"
Revealed type is "def (name: builtins.str, *, greeting: builtins.str =) -> builtins.str"
```

๐Ÿ’ก **Challenge Time!**
Create a generic `map_tuple` function using `TypeVarTuple` that takes a function `f` and an arbitrary tuple of arguments `*args`, applying `f` to each argument and returning a new tuple with the results. For example: `map_tuple(str, (1, 2, 3))` should return `('1', '2', '3')`. How would you type hint `f` if it takes a single argument and returns a single value?

---
Which special method within a Python descriptor is responsible for defining the behavior when an attribute is *assigned* (e.g., `instance.attribute = value`)?
Anonymous Quiz
0%
__get__(self, instance, owner)
0%
__set__(self, instance, value)
0%
__set_name__(self, owner, name)
0%
__init__(self, name)
๐Ÿš€ **Day 56: Surgical Code Transformation with Python AST!** ๐Ÿ Ever wondered what happens *behind the scenes* when Python executes your code? The Abstract Syntax Tree (AST) is your answer! It's a fundamental, hierarchical representation of your code's structure. Today, we're not just looking at it; we're *hacking* it! ๐Ÿ› ๏ธ Learn to programmatically analyze, modify, and even generate Python code by manipulating its AST. Unlock powerful capabilities for linters, refactoring tools, and advanced metaprogramming! Get ready to dissect your code like never before. ๐Ÿ’ก

```python
import ast

# 1. Original code as a string
original_code_str = """
def calculate_price(quantity):
base_price = 10
total = base_price * quantity + 5 # Let's change this '5'
return total
"""

print("--- Original Code ---")
print(original_code_str)

# 2. Parse the code into an AST
tree = ast.parse(original_code_str)

# 3. Define a custom NodeTransformer to modify the AST
class ConstantModifier(ast.NodeTransformer):
"""
Changes all integer constants with value '5' to '500'.
"""
def visit_Constant(self, node):
# ast.Constant is used for literals like numbers, strings, etc. (Python 3.8+)
if isinstance(node.value, int) and node.value == 5:
# Create a new Constant node with the desired value
return ast.Constant(value=500, kind=None)
# For all other nodes, ensure children are visited
return self.generic_visit(node)

# 4. Apply the transformer to modify the AST
modified_tree = ConstantModifier().visit(tree)

# 5. Unparse the modified AST back into a code string
modified_code_str = ast.unparse(modified_tree)

print("n--- Modified Code AST ---")
print(modified_code_str)

# 6. Execute both versions of the code to see the difference
print("n--- Execution Results ---")

# Execute original code
exec_globals_original = {}
exec(original_code_str, exec_globals_original)
original_calculate_price = exec_globals_original['calculate_price']
print(f"calculate_price(2) (original): {original_calculate_price(2)}") # Expected: (10*2)+5 = 25

# Execute modified code
exec_globals_modified = {}
exec(modified_code_str, exec_globals_modified)
modified_calculate_price = exec_globals_modified['calculate_price']
print(f"calculate_price(2) (modified): {modified_calculate_price(2)}") # Expected: (10*2)+500 = 520
```

Output example:
```
--- Original Code ---

def calculate_price(quantity):
base_price = 10
total = base_price * quantity + 5 # Let's change this '5'
return total

--- Modified Code AST ---

def calculate_price(quantity):
base_price = 10
total = base_price * quantity + 500
return total

--- Execution Results ---
calculate_price(2) (original): 25
calculate_price(2) (modified): 520
```

**Small Challenge:**
Instead of changing a constant, can you modify the `calculate_price` function's AST to accept an additional argument `tax_rate` and add `total * tax_rate` to the return value? ๐Ÿคฏ (Hint: You'll need to add a new `arg` to `FunctionDef.args.args` and modify the `Return` statement's `value` to be a new `BinOp`). Share your solution! ๐Ÿง 

---
Here's your Telegram post for Day 57 of the Python learning series!

---

๐Ÿ”ฅ Day 57: Async Seams! ๐Ÿงต Ever wondered how to integrate the speed of `asyncio` into an existing Python codebase full of blocking, synchronous calls? ๐Ÿค” Today, we're diving into 'Asynchronous Seams' โ€“ a crucial technique for gracefully weaving non-blocking I/O into legacy synchronous codebases! โœจ Learn to leverage `asyncio.to_thread()` (or `loop.run_in_executor`) to run blocking operations in a separate thread, keeping your event loop responsive and your application blazing fast. No more freezing your async tasks with a slow database query or a heavy file read!

```python
import asyncio
import time
import functools

# --- Simulate a "legacy" synchronous blocking I/O operation ---
def sync_blocking_io(task_id: int, duration: int):
"""Simulates a blocking operation that takes 'duration' seconds."""
print(f"[{time.time():.2f}] ๐Ÿข Blocking Task {task_id} starting for {duration}s...")
time.sleep(duration) # Simulates a blocking call (e.g., network, disk I/O, heavy computation)
print(f"[{time.time():.2f}] โœ… Blocking Task {task_id} finished.")
return f"Result from Blocking Task {task_id} (duration: {duration}s)"

# --- Regular async tasks ---
async def async_worker(name: str, delay: int):
"""An asynchronous task that pauses for 'delay' seconds."""
print(f"[{time.time():.2f}] ๐Ÿš€ Async Worker {name} starting for {delay}s...")
await asyncio.sleep(delay)
print(f"[{time.time():.2f}] โญ Async Worker {name} finished.")
return f"Result from Async Worker {name} (delay: {delay}s)"

async def main():
print("--- Starting main async process ---")
start_time = time.time()

# We use asyncio.to_thread() (Python 3.9+) to run the blocking function
# in a separate thread, preventing it from blocking the main event loop.
# For older Python versions, you'd typically use:
# loop = asyncio.get_running_loop()
# blocking_task_coro = loop.run_in_executor(None, functools.partial(sync_blocking_io, 1, 3))

blocking_task_coro = asyncio.to_thread(sync_blocking_io, 1, 3)

# Run multiple async tasks and the "threaded" blocking task concurrently
results = await asyncio.gather(
async_worker("A", 1),
async_worker("B", 2),
blocking_task_coro, # The blocking task is now awaitable!
async_worker("C", 0.5)
)

print("n--- All tasks completed ---")
for res in results:
print(f"Final Result: {res}")
print(f"Total elapsed time: {time.time() - start_time:.2f}s")

if __name__ == "__main__":
asyncio.run(main())
```

Output example:
```
--- Starting main async process ---
[1678886400.00] ๐Ÿš€ Async Worker A starting for 1s...
[1678886400.00] ๐Ÿš€ Async Worker B starting for 2s...
[1678886400.00] ๐Ÿš€ Async Worker C starting for 0.5s...
[1678886400.00] ๐Ÿข Blocking Task 1 starting for 3s...
[1678886400.50] โญ Async Worker C finished.
[1678886401.00] โญ Async Worker A finished.
[1678886402.00] โญ Async Worker B finished.
[1678886403.00] โœ… Blocking Task 1 finished.

--- All tasks completed ---
Final Result: Result from Async Worker A (delay: 1s)
Final Result: Result from Async Worker B (delay: 2s)
Final Result: Result from Blocking Task 1 (duration: 3s)
Final Result: Result from Async Worker C (delay: 0.5s)
Total elapsed time: 3.00s
```
Notice how the async workers finish *before* the blocking task, even though the blocking task started first. This demonstrates that `asyncio.to_thread` prevented the blocking call from freezing the event loop!

๐Ÿ’ก Small Challenge: Take your `asyncio` skills further! Try wrapping a synchronous network request library (like `requests`) using `asyncio.to_thread` or `run_in_executor` to make an external API call from within an `asyncio` application without blocking the event loop. Share your async masterpiece! ๐Ÿš€

---
Hello Every one WE will Restart from day 1
Here's your Telegram post for Day 3 of the Python learning series!

---

Hey Python Learners! ๐Ÿ‘‹ Ready for Day 3? Today we're diving into the powerful world of *Operators*! ๐Ÿš€ Operators are special symbols that perform operations on values and variables. From basic math to complex logic, they are the building blocks of any program. Let's see how they work and level up our coding skills! ๐Ÿ‘จโ€๐Ÿ’ป

```python
# Day 3: Operators in Python - Let's compute! โœจ

# Arithmetic Operators
a = 10
b = 3
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 (float result)
print(f"a // b = {a // b}") # Floor Division (integer result)
print(f"a % b = {a % b}") # Modulus (remainder)
print(f"a ** b = {a ** b}") # Exponentiation

# Comparison Operators
x = 5
y = 8
print(f"x == y is {x == y}") # Equal to
print(f"x < y is {x < y}") # Less than
print(f"x >= y is {x >= y}") # Greater than or Equal to

# Logical Operators
is_logged_in = True
has_permission = False
print(f"Logged in AND has permission: {is_logged_in and has_permission}")
print(f"Logged in OR has permission: {is_logged_in or has_permission}")
print(f"NOT logged in: {not is_logged_in}")
```

Output example:
```
a + b = 13
a - b = 7
a * b = 30
a / b = 3.3333333333333335
a // b = 3
a % b = 1
a ** b = 1000
x == y is False
x < y is True
x >= y is False
Logged in AND has permission: False
Logged in OR has permission: True
NOT logged in: False
```

๐Ÿ’ก *Challenge Time!* What will be the output of the expression `(15 % 4) * (2 ** 3) + 7 // 2`? Try it in your interpreter and share your answer! ๐Ÿค” Let's see who gets it right! ๐Ÿ‘‡

---
Day 3: Operators in Python \- Code & Output
What is the output of the following Python expression: `5 * 2 + 8 / 4`?
Anonymous Quiz
0%
12.0
0%
12
0%
7.0
0%
7
๐ŸŽ‰ Day 5: Let's Get Loopy! ๐ŸŽ‰

Ready to make your code work smarter, not harder? Today, we're diving into the powerful world of `for` and `while` loops! ๐Ÿ”„ These fundamental tools allow your programs to repeat tasks, iterate through collections, and automate workflows effortlessly. Master them, and unlock a new level of Python proficiency! โœจ Let's loop into action! ๐Ÿ‘‡

```python
# --- Example 1: The 'for' Loop ---
print("Counting with 'for' loop:")
for i in range(1, 4): # Loops from 1 up to (but not including) 4
print(f"Number: {i}")

fruits = ["apple", "banana", "cherry"]
print("nIterating through a list with 'for' loop:")
for fruit in fruits:
print(f"I like {fruit}!")

# --- Example 2: The 'while' Loop ---
print("nCounting down with 'while' loop:")
count = 3
while count > 0:
print(f"T-minus {count}...")
count -= 1 # Decrement count by 1
print("Liftoff! ๐Ÿš€")
```

Output example:
```
Counting with 'for' loop:
Number: 1
Number: 2
Number: 3

Iterating through a list with 'for' loop:
I like apple!
I like banana!
I like cherry!

Counting down with 'while' loop:
T-minus 3...
T-minus 2...
T-minus 1...
Liftoff! ๐Ÿš€
```

๐Ÿ’ก Your turn! Can you use a `for` loop to print all even numbers from 2 to 10 (inclusive)? Or use a `while` loop to continuously ask a user for their favorite color until they type "exit"? Share your code! ๐Ÿ‘‡

---
Day 5: Loops \(for and while\) \- Code & Output
What will be the output of the following Python code snippet?
```python
counter = 0 while counter < 4: print(counter) counter += 1 ```
Anonymous Quiz
0%
0 1 2 3
0%
1 2 3 4
0%
0 1 2 3 4
0%
The code will result in an infinite loop.
๐Ÿ‘‹ Hey Python Learners!

Welcome to **Day 6** of our Python journey! ๐Ÿš€ Today, we're diving into one of the most fundamental and powerful concepts in programming: **Functions**.

Functions are reusable blocks of code that perform a specific task. They help organize your code, make it more readable, and save you from repeating yourself (DRY - Don't Repeat Yourself principle!). Think of them as mini-programs within your main program. Let's see how to define and use them!

```python
# Define a simple function to greet someone
def greet_user(name):
"""This function takes a name and prints a greeting."""
print(f"Hello, {name}! Welcome to Day 6. ๐Ÿ‘‹")

# Define a function that adds two numbers and returns the result
def add_numbers(num1, num2):
"""This function adds two numbers and returns their sum."""
return num1 + num2

# Call the greet_user function
greet_user("Alice")
greet_user("Bob")

# Call the add_numbers function and store the result
result = add_numbers(10, 5)
print(f"The sum of 10 and 5 is: {result}")

# You can also call it directly in a print statement
print(f"Adding 7 and 3 gives: {add_numbers(7, 3)}")
```

Output example:
```
Hello, Alice! Welcome to Day 6. ๐Ÿ‘‹
Hello, Bob! Welcome to Day 6. ๐Ÿ‘‹
The sum of 10 and 5 is: 15
Adding 7 and 3 gives: 10
```

---

### ๐Ÿ”ฅ Small Challenge:
Write a Python function called `calculate_area` that takes two arguments: `length` and `width`. The function should return the product of these two numbers (the area of a rectangle). Then, call your function with example values and print the result!

---

๐Ÿ’ก Keep practicing, and you'll master functions in no time! See you tomorrow for Day 7! #PythonLearning #Functions #PythonBasics #CodingChallenge
Day 6: Functions in Python \- Code & Output
Which keyword is used to define a function in Python?
Anonymous Quiz
0%
func
0%
define
0%
def
0%
function
Here's your Telegram post for Day 7 of the Python learning series!

---

๐Ÿ‘‹ Day 7 of our Python journey is here! Today, we're diving into two fundamental data structures: **Lists** ๐Ÿ“ and **Tuples** ๐Ÿง.

Lists are like flexible shopping carts ๐Ÿ›’ - ordered, changeable, and you can add or remove items easily. Tuples, on the other hand, are like sealed time capsules ๐Ÿ•ฐ๏ธ - ordered, but once created, their contents cannot be changed. Understanding their differences is key to writing efficient Python code! Let's explore!

```python
# --- Lists: Mutable collections ---
my_list = ["apple", "banana", "cherry"]
print(f"Original List: {my_list}")

# Add an element
my_list.append("date")
print(f"List after append: {my_list}")

# Modify an element
my_list[0] = "apricot"
print(f"List after modification: {my_list}")

# Access an element
print(f"Second item in list: {my_list[1]}")
print(f"List length: {len(my_list)}")

print("-" * 20)

# --- Tuples: Immutable collections ---
my_tuple = ("red", "green", "blue")
print(f"Original Tuple: {my_tuple}")

# Access an element
print(f"First item in tuple: {my_tuple[0]}")
print(f"Tuple length: {len(my_tuple)}")

# Uncommenting the line below would cause an error (TypeError)
# my_tuple[0] = "purple" # Tuples cannot be modified after creation!
# print(f"Attempted tuple modification (will raise error): {my_tuple}")
```

Output example:
```
Original List: ['apple', 'banana', 'cherry']
List after append: ['apple', 'banana', 'cherry', 'date']
List after modification: ['apricot', 'banana', 'cherry', 'date']
Second item in list: banana
List length: 4
--------------------
Original Tuple: ('red', 'green', 'blue')
First item in tuple: red
Tuple length: 3
```

๐Ÿš€ **Your Challenge:**
1. Create a list called `my_favorite_movies` with 3 movie titles.
2. Add another movie to your `my_favorite_movies` list.
3. Access and print the first movie in your list.
4. Create a tuple called `my_birth_date` with your birth year, month, and day (e.g., `(1990, 5, 15)`). Can you change any part of `my_birth_date`? ๐Ÿค”

---