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 32: Unlock the ultimate power of Python! 🌟 Ever wondered how frameworks create magical APIs or enforce specific class structures? Today, we dive deep into the #MetaclassMultiverse! 🤯 We'll explore how to forge custom type systems, going far beyond simple `type` calls to control class creation itself. Get ready to redefine what a Python class can be! Let's build some powerful magic together. ✨ #PythonLearning #AdvancedPython #Metaclasses

Metaclasses allow you to intercept class creation and inject behavior, attributes, or methods before the class is even fully defined. This enables powerful patterns for validation, registration, and framework building!

```python
import datetime

# 🚀 Our custom metaclass
class MyMeta(type):
"""
A metaclass that automatically adds a creation timestamp and a 'greet' method
to any class that uses it.
"""
def __new__(cls, name, bases, dct):
# 1. Add a creation timestamp to the class dictionary
dct['_creation_timestamp'] = datetime.datetime.now()

# 2. Add a default 'greet' method if not already defined
if 'greet' not in dct:
def greet_method(self):
return (f"Hello from {self.__class__.__name__}! "
f"Created at {self._creation_timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
dct['greet'] = greet_method

# 3. Call the base type's __new__ to complete class creation
return super().__new__(cls, name, bases, dct)

# 🧙‍♀️ Classes using our custom metaclass
class Character(metaclass=MyMeta):
def __init__(self, name):
self.name = name

class Item(metaclass=MyMeta):
def __init__(self, name):
self.name = name

# This class defines its own greet, which will override the metaclass's
def greet(self):
return f"Hello, I am {self.name}, an item created magically!"

# ✨ Let's see them in action!
hero = Character("Alice")
sword = Item("Excalibur")

print(hero.greet())
print(sword.greet()) # Notice how Item's custom greet takes precedence!
print(f"Character created at: {Character._creation_timestamp}")
```

Output example:
```
Hello from Character! Created at 2023-10-27 10:30:00 # (Timestamp will vary)
Hello, I am Excalibur, an item created magically!
Character created at: 2023-10-27 10:30:00 # (Timestamp will vary)
```

💡 **Your Challenge:** Modify `MyMeta` to automatically add a class-level `_registry` dictionary. For every class that uses `MyMeta`, register the class itself in this `_registry` using its `name` as the key. How would you then access this registry from another part of your code to list all registered classes?

---
Which of the following special methods in Python is invoked when an attribute is accessed on an object, *regardless* of whether the attribute exists on the object or its class, making it ideal for comprehensive proxying?
Anonymous Quiz
100%
A) __getattr__
0%
B) __getattribute__
0%
C) __call__
0%
D) __dir__
🎉 Day 34: Unlocking Python's Object Model! 🐍 Ever wondered how `@property` works its magic, or why methods behave like they do? Today, we dive into the **Descriptor Protocol**! 🕵️‍♂️ This powerful mechanism allows you to customize attribute access (`__get__`, `__set__`, `__delete__`) for class variables. Master descriptors, and you'll gain unparalleled control over your objects' behavior, laying the groundwork for elegant and robust designs. Ready to peek behind the curtain? ✨

```python
class LoggingDescriptor:
"""
A simple descriptor that logs attribute access.
"""
def __set_name__(self, owner, name):
# Called when the descriptor is assigned to a class attribute
self.public_name = name
self.private_name = '_' + name # Store value in a private attribute

def __get__(self, obj, obj_type=None):
# Called when the attribute is accessed (e.g., obj.value)
if obj is None:
return self # Accessing descriptor directly from class (MyClass.value)
print(f"Accessing '{self.public_name}' from {obj_type.__name__}")
return getattr(obj, self.private_name, None)

def __set__(self, obj, value):
# Called when the attribute is assigned (e.g., obj.value = 10)
print(f"Setting '{self.public_name}' to {value} in {obj.__class__.__name__}")
setattr(obj, self.private_name, value)

def __delete__(self, obj):
# Called when the attribute is deleted (e.g., del obj.value)
print(f"Deleting '{self.public_name}' from {obj.__class__.__name__}")
delattr(obj, self.private_name)

class MyClass:
value = LoggingDescriptor() # 'value' is an instance of our descriptor
another_value = LoggingDescriptor()

def __init__(self, initial_value):
self.value = initial_value # This assignment triggers LoggingDescriptor.__set__

# Demonstrate usage
print("--- Creating instance ---")
obj = MyClass(10)

print("n--- Accessing attributes ---")
print(f"obj.value: {obj.value}")
print(f"obj.another_value: {obj.another_value}") # Accessing an unset descriptor

print("n--- Modifying attribute ---")
obj.value = 20

print("n--- Accessing again ---")
print(f"obj.value: {obj.value}")

print("n--- Deleting attribute ---")
del obj.value

print("n--- Accessing after delete ---")
try:
print(obj.value)
except AttributeError as e:
print(f"Error accessing deleted attribute: {e}")

print("n--- Accessing descriptor directly from class ---")
print(MyClass.value)
```

Output example:
```
--- Creating instance ---
Setting 'value' to 10 in MyClass

--- Accessing attributes ---
Accessing 'value' from MyClass
obj.value: 10
Accessing 'another_value' from MyClass
obj.another_value: None

--- Modifying attribute ---
Setting 'value' to 20 in MyClass

--- Accessing again ---
Accessing 'value' from MyClass
obj.value: 20

--- Deleting attribute ---
Deleting 'value' from MyClass

--- Accessing after delete ---
Error accessing deleted attribute: 'MyClass' object has no attribute '_value'

--- Accessing descriptor directly from class ---
<__main__.LoggingDescriptor object at 0x...>
```

💡 **Your Challenge:** Modify the `LoggingDescriptor` to add **type validation**. For instance, ensure `value` can only be set to integers. If a non-integer is assigned, raise a `TypeError`! Can you make it even more generic to accept a type hint during descriptor instantiation (e.g., `value = ValidatedDescriptor(int)`)? 🚀
Which of the following methods are specifically part of the Descriptor Protocol in Python, controlling attribute access?
Anonymous Quiz
0%
A) __call__
0%
B) __get__
0%
C) __init__
0%
D) __str__
Hey Pythonistas! 🐍 Get ready to unlock some serious processing power!

### Day 35: Coroutine Superpowers: Building High-Performance Data Pipelines with Asynchronous Generators 🚀

Beyond basic `asyncio`, today we dive into the magic of **Asynchronous Generators**. ✨ They combine the power of `async/await` for non-blocking operations with the memory efficiency of generators, making them perfect for building high-performance, back-pressured data pipelines. Imagine processing huge datasets or real-time streams without blocking your event loop! Let's see how to chain these powerful constructs for efficient data flow. ⚡️

```python
import asyncio
import random

async def produce_data(count: int, delay: float):
"""An asynchronous generator that yields simulated data."""
print("Producer started 📦")
for i in range(1, count + 1):
# Simulate async I/O or a network call
await asyncio.sleep(delay + random.uniform(0, 0.1))
print(f" Producer yielded: {i}")
yield i
print("Producer finished ✅")

async def process_data(data_stream):
"""An asynchronous generator that processes data from another async generator."""
print("Processor started ⚙️")
async for item in data_stream:
if item % 2 == 0: # Only process even numbers
processed_item = item * 10
print(f" Processor transformed: {item} -> {processed_item}")
yield processed_item
else:
print(f" Processor skipped: {item} (odd)")
print("Processor finished ✅")

async def consume_data(processed_stream):
"""An asynchronous function that consumes data from an async generator."""
print("Consumer started 📈")
total_processed = 0
async for item in processed_stream:
print(f" Consumer received: {item}")
total_processed += 1
print(f"Consumer finished. Total items received: {total_processed} ✅")

async def main():
print("--- Starting Pipeline ---")
data_source = produce_data(5, 0.2)
# Chain async generators: producer -> processor -> consumer
processed_pipeline = process_data(data_source)
await consume_data(processed_pipeline)
print("--- Pipeline Finished ---")

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

```

Output example:
```
--- Starting Pipeline ---
Consumer started 📈
Processor started ⚙️
Producer started 📦
Producer yielded: 1
Processor skipped: 1 (odd)
Producer yielded: 2
Processor transformed: 2 -> 20
Consumer received: 20
Producer yielded: 3
Processor skipped: 3 (odd)
Producer yielded: 4
Processor transformed: 4 -> 40
Consumer received: 40
Producer yielded: 5
Processor skipped: 5 (odd)
Producer finished ✅
Processor finished ✅
Consumer finished. Total items received: 2 ✅
--- Pipeline Finished ---
```

**Small Challenge:**
Add another asynchronous generator `log_data(processed_stream)` between `process_data` and `consume_data`. This new generator should simply print each item it receives before yielding it again, simulating logging or inspection without modification. Chain it into the pipeline! 🕵️‍♀️

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

---

🔥🐍 **Day 36: Cracking the CPython Egg!** 🤯🚀

Ready to peel back the layers of Python and truly understand how it works under the hood? Today, we're not just coding; we're *cracking the CPython egg* and exploring the advanced mechanisms that power your applications. Get ready to peek into bytecode, manipulate runtime, and sculpt complex systems with meta-programming. This isn't your average Python – it's the stuff that builds robust frameworks! ⚙️🔍

Let's start by looking at Python's bytecode, the low-level instructions the CPython interpreter executes. The `dis` module is our secret weapon here!

```python
import dis

def calculate_and_decide(a, b):
"""
A simple function to demonstrate Python bytecode.
It calculates a sum and applies a conditional transformation.
"""
intermediate_result = a + b
if intermediate_result > 10:
return intermediate_result * 2
else:
return intermediate_result + 1

print("--- Disassembling 'calculate_and_decide' function ---")
dis.dis(calculate_and_decide)
```

Output example:
```
--- Disassembling 'calculate_and_decide' function ---
8 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 STORE_FAST 2 (intermediate_result)

9 8 LOAD_FAST 2 (intermediate_result)
10 LOAD_CONST 1 (10)
12 COMPARE_OP 4 (>)
14 POP_JUMP_IF_FALSE 24

10 16 LOAD_FAST 2 (intermediate_result)
18 LOAD_CONST 2 (2)
20 BINARY_MULTIPLY
22 RETURN_VALUE

12 24 LOAD_FAST 2 (intermediate_result)
26 LOAD_CONST 3 (1)
28 BINARY_ADD
30 RETURN_VALUE
```

### Small Challenge:
Try disassembling a function that uses a `for` loop or a `while` loop. Can you identify the bytecode instructions responsible for the iteration, comparison, and jumping logic? Share your insights and which specific instructions you found interesting!

---
🚀 Day 37 Alert! Today, we're diving deep into Python's performance secrets with **Zero-Cost Abstractions**. Ever wondered how to squeeze every byte and cycle out of your code? We'll uncover techniques like `__slots__` that dramatically reduce memory footprint by eliminating `__dict__`, bringing C-like efficiency to your Python objects. Get ready to sculpt leaner, faster applications! ⚡️

```python
import sys

# Class without __slots__
# Each instance has its own __dict__ for attributes
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y

# Class with __slots__
# Attributes are stored directly in the instance, no __dict__
class SlottedPoint:
__slots__ = ('x', 'y') # Explicitly declare attributes
def __init__(self, x, y):
self.x = x
self.y = y

# Demonstrate memory usage
p_regular = RegularPoint(10, 20)
p_slotted = SlottedPoint(10, 20)

print(f"Memory size of RegularPoint: {sys.getsizeof(p_regular)} bytes")
print(f"Memory size of SlottedPoint: {sys.getsizeof(p_slotted)} bytes")

# Characteristic: __slots__ prevents adding new attributes dynamically
try:
p_slotted.z = 30
except AttributeError as e:
print(f"nAttempted to add new attribute to SlottedPoint: {e}")

# Check for __dict__ presence
print(f"RegularPoint has __dict__: {'__dict__' in p_regular.__dir__()}")
print(f"SlottedPoint has __dict__: {'__dict__' in p_slotted.__dir__()}")
```

Output example:
```
Memory size of RegularPoint: 56 bytes
Memory size of SlottedPoint: 40 bytes

Attempted to add new attribute to SlottedPoint: 'SlottedPoint' object has no attribute 'z'
RegularPoint has __dict__: True
SlottedPoint has __dict__: False
```

💡 **Your Challenge:** How does `__slots__` impact inheritance? What happens if a subclass *doesn't* define `__slots__` but its parent does? Share your thoughts! 👇

---
🚀 Day 38: Deconstructing Python with AST! 🚀

Day 38 is here, and we're diving deep into Python's soul! 🧠 Today, we're not just writing Python; we're *rewriting* Python from its very core. We'll explore the Abstract Syntax Tree (AST), the internal representation of your code. Learn how to parse, traverse, and even modify your Python programs programmatically. This is powerful stuff for building custom linters, static analyzers, code generators, or even domain-specific languages. Get ready to deconstruct and reconstruct! 🛠️🔍

```python
import ast
import inspect

# A simple Python function to analyze
def example_function(x):
result = x * 2
return result + 1

# Convert the function's source code to an AST
source_code = inspect.getsource(example_function)
parsed_ast = ast.parse(source_code)

# --- AST Transformation Example ---
class FunctionTransformer(ast.NodeTransformer):
"""
A NodeTransformer that renames 'example_function' to 'transformed_function'
and inserts a print statement at the beginning of its body.
"""
def visit_FunctionDef(self, node):
if node.name == 'example_function':
node.name = 'transformed_function'
# Create a print statement: print("Function transformed via AST!")
print_call = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[ast.Constant(value="Function transformed via AST!")],
keywords=[]
)
)
# Add the print statement at the beginning of the function body
node.body.insert(0, print_call)
self.generic_visit(node) # Ensure we visit child nodes of the function
return node

transformer = FunctionTransformer()
modified_ast = transformer.visit(parsed_ast)

# Unparse the modified AST back into Python code
unparsed_code = ast.unparse(modified_ast)
print("--- Original Code ---")
print(source_code)
print("n--- Transformed Code ---")
print(unparsed_code)

# Compile and execute the transformed code to verify its functionality
print("n--- Executing Transformed Function ---")
exec_globals = {} # A dictionary to capture the defined function
exec(compile(modified_ast, '<string>', 'exec'), exec_globals)

# Now, call the transformed function from the exec_globals dict
if 'transformed_function' in exec_globals:
print(f"Result from transformed_function(5): {exec_globals['transformed_function'](5)}")

```

Output example:
```
--- Original Code ---
def example_function(x):
result = x * 2
return result + 1

--- Transformed Code ---
def transformed_function(x):
print('Function transformed via AST!')
result = x * 2
return result + 1

--- Executing Transformed Function ---
Function transformed via AST!
Result from transformed_function(5): 11
```

Challenge:
Your Challenge: Modify the `FunctionTransformer` to change all integer literals (e.g., `2` and `1` in `example_function`) within any function to their float equivalents (e.g., `2.0` and `1.0`). Hint: You'll need to override `visit_Constant` and check if its `value` is an `int`. Good luck, AST wizard! ✨

---
Which `ast` module class is primarily used for modifying nodes during AST traversal, allowing for transformation of the code structure?
Anonymous Quiz
0%
ast.NodeTransformer
0%
ast.NodeVisitor
0%
ast.ASTWalker
0%
ast.CodeModifier
Hey Pythonistas! 👋 Ever wondered how `property()`, `classmethod()`, or even regular methods work their magic? ✨ Today, on Day 39, we're unlocking the secrets of Python's **Descriptor Protocol**!

This advanced concept allows objects to customize attribute access (`__get__`, `__set__`, `__delete__`), going far beyond simple `__getattr__` or `__setattr__`. Understanding descriptors is key to grasping Python's object model at a much deeper level and writing truly powerful, reusable code. Ready to empower your classes? Let's dive in! 🚀

```python
import types

class ValueLogger:
"""A descriptor that logs attribute access, assignment, and deletion."""
def __init__(self, name):
# We need an internal place to store the actual value.
# In a real scenario, this might store data in the instance's __dict__
# or in a weakref map keyed by instance. For simplicity, we'll store it here.
self._name = name
self._value = None

def __get__(self, instance, owner):
if instance is None:
# Accessing descriptor via the class (e.g., MyClass.my_attr)
print(f"[{self._name}] Accessing descriptor via class {owner.__name__}")
return self
# Accessing descriptor via an instance (e.g., obj.my_attr)
print(f"[{self._name}] Getting value for {instance.__class__.__name__}.{self._name}")
return self._value

def __set__(self, instance, value):
# Setting an attribute on an instance (e.g., obj.my_attr = 42)
print(f"[{self._name}] Setting value for {instance.__class__.__name__}.{self._name} to '{value}'")
self._value = value

def __delete__(self, instance):
# Deleting an attribute on an instance (e.g., del obj.my_attr)
print(f"[{self._name}] Deleting value for {instance.__class__.__name__}.{self._name}")
del self._value

class Account:
balance = ValueLogger('balance') # Assigning a descriptor instance

def __init__(self, initial_balance):
self.balance = initial_balance # This calls ValueLogger.__set__

# --- Demonstration ---
print("--- Creating Account and setting initial balance ---")
my_account = Account(100)
print(f"Initial balance is: {my_account.balance}") # Calls ValueLogger.__get__

print("n--- Changing balance ---")
my_account.balance = 150 # Calls ValueLogger.__set__
print(f"New balance is: {my_account.balance}") # Calls ValueLogger.__get__

print("n--- Accessing descriptor via class ---")
print(Account.balance) # Calls ValueLogger.__get__ with instance=None

print("n--- Deleting balance ---")
del my_account.balance # Calls ValueLogger.__delete__

try:
print(my_account.balance) # This will likely fail now that _value is deleted
except AttributeError:
print("Attribute 'balance' successfully deleted from my_account.")

```

Output example:
```
--- Creating Account and setting initial balance ---
[balance] Setting value for Account.balance to '100'
[balance] Getting value for Account.balance
Initial balance is: 100

--- Changing balance ---
[balance] Setting value for Account.balance to '150'
[balance] Getting value for Account.balance
New balance is: 150

--- Accessing descriptor via class ---
[balance] Accessing descriptor via class Account
<__main__.ValueLogger object at 0x...>

--- Deleting balance ---
[balance] Deleting value for Account.balance
Attribute 'balance' successfully deleted from my_account.
```

### Small Challenge:
Can you modify the `ValueLogger` descriptor to enforce **type checking**? For example, make `balance` always an `int` or `float`, raising a `TypeError` if a different type is assigned. 🕵️‍♂️ Alternatively, explore the `__set_name__` method (added in Python 3.6) to automatically get the attribute's name without needing to pass it in `__init__`! 💡

---
Which of the following built-in Python features *primarily* relies on the descriptor protocol for its functionality?
Anonymous Quiz
0%
A) The `property()` decorator
0%
B) The `__getattr__` special method
0%
C) The `try...except` block for error handling
0%
D) List comprehensions
🚀 **Day 40: Synthesizing Reality - Dynamic Code Generation with AST Manipulation!** 🤯

We've reached the apex! For our grand finale, let's explore one of Python's most mind-bending capabilities: **dynamic code generation**. Forget just defining classes; today, we're building and executing code *from scratch* at runtime using Python's Abstract Syntax Tree (AST) and the mighty `exec` function. This isn't just theory; it's the foundation for DSLs, high-performance runtime optimizations, and even self-modifying programs! Are you ready to dive into the ultimate metaprogramming challenge? 🛠️✨

```python
import ast
import inspect
import sys

# Our goal: Dynamically create a function like this:
# def greet_dynamic(name):
# print(f"Hello, {name}! This code was generated on the fly.")

# Step 1: Build the Abstract Syntax Tree (AST) for the function.
# This part manually constructs the AST nodes representing the function.
func_name_node = ast.Name(id='greet_dynamic', ctx=ast.Store())
arg_node = ast.arg(arg='name', annotation=None)
args_node = ast.arguments(posonlyargs=[], args=[arg_node], vararg=None,
kwonlyargs=[], kw_defaults=[], defaults=[])

# The f-string: f"Hello, {name}! This code was generated on the fly."
f_string_parts = [
ast.Constant(value="Hello, "),
ast.FormattedValue(value=ast.Name(id='name', ctx=ast.Load()), conversion=-1, format_spec=None),
ast.Constant(value="! This code was generated on the fly.")
]
f_string_node = ast.JoinedStr(values=f_string_parts)

# The print statement: print(...)
print_call_node = ast.Expr(value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[f_string_node],
keywords=[]
))

# The function body
func_body_nodes = [print_call_node]

# The function definition node
func_def_node = ast.FunctionDef(
name=func_name_node.id,
args=args_node,
body=func_body_nodes,
decorator_list=[],
returns=None,
type_comment=None
)

# Step 2: Create a module AST that contains our function definition.
module_node = ast.Module(body=[func_def_node], type_ignores=[])

# Step 3: Compile the AST into a code object.
# The '<string>' argument is the filename for traceback purposes.
compiled_code_obj = compile(module_node, '<string>', 'exec')

# Step 4: Execute the compiled code in a new environment.
# `exec_env` will store the definitions created by `exec`.
exec_env = {}
exec(compiled_code_obj, exec_env)

# Step 5: Retrieve and use the dynamically created function.
dynamic_function = exec_env['greet_dynamic']

print("--- Dynamically Generated Code ---")
# Use ast.unparse (Python 3.9+) to show what the AST represents
# For older Python versions, this part needs manual reconstruction or a custom visitor.
if sys.version_info >= (3, 9):
print(ast.unparse(module_node))
else:
# Fallback for Python versions < 3.9
print("# ast.unparse requires Python 3.9+ to directly visualize the AST.")
print("def greet_dynamic(name):")
print(" print(f"Hello, {name}! This code was generated on the fly.")")
print("----------------------------------")

print("n--- Executing Generated Function ---")
# Call the function
dynamic_function("Pythonista")
```

Output example:
```
--- Dynamically Generated Code ---
def greet_dynamic(name):
print(f'Hello, {name}! This code was generated on the fly.')
----------------------------------

--- Executing Generated Function ---
Hello, Pythonista! This code was generated on the fly.
```

✨ **Challenge for Day 40:**
Can you modify the AST to make `greet_dynamic` also print the current timestamp *before* its greeting? (Hint: You'll need to add an `ast.Import` for `datetime` and an additional `ast.Expr` node containing a call to `datetime.datetime.now()` to the function body! Remember to add the `ast.Import` at the module level.)
Which of the following methods are part of Python's Descriptor Protocol, allowing customization of attribute access?
Anonymous Quiz
0%
A. `__iter__`, `__next__`
0%
B. `__enter__`, `__exit__`
0%
C. `__get__`, `__set__`, `__delete__`
0%
D. `__str__`, `__repr__`
Here's your Telegram post for Day 43!

---

🌟 **Day 43: The Zen of Descriptors - Crafting Intuitive & Efficient Attribute Access** 🌟

Ever wondered how `@property`, methods, or even `staticmethod`s work their magic under the hood? ✨ The secret often lies with **descriptors**! These powerful Python objects allow you to customize attribute access, setting, and deletion for class instances.

Descriptors empower you to design cleaner APIs, enforce validation rules, implement caching, or create sophisticated attribute behaviors with elegant reuse. It's a key concept for building robust and "Pythonic" code, often used implicitly in popular frameworks!

Let's dive into creating our own descriptor for validated attributes! 👇

```python
import numbers

class ValidatedNumber:
"""A descriptor that ensures an attribute is a positive number."""
def __set_name__(self, owner, name):
# __set_name__ is called when the descriptor is assigned to a class attribute.
# It's perfect for knowing the attribute's name in the owner class.
self.public_name = name
self.private_name = '_' + name # Store value in a private attribute

def __get__(self, obj, obj_type=None):
if obj is None: # Accessing descriptor directly from class (e.g., Product.price)
return self
return getattr(obj, self.private_name)

def __set__(self, obj, value):
if not isinstance(value, numbers.Number):
raise TypeError(f"'{self.public_name}' must be a number.")
if value <= 0:
raise ValueError(f"'{self.public_name}' must be positive.")
setattr(obj, self.private_name, value)

def __delete__(self, obj):
delattr(obj, self.private_name)

class Product:
price = ValidatedNumber()
quantity = ValidatedNumber()

def __init__(self, price: float, quantity: int):
self.price = price
self.quantity = quantity

def get_total_cost(self) -> float:
return self.price * self.quantity

```

**Output example:**
```
# ✅ Valid product creation
laptop = Product(price=1200.50, quantity=2)
print(f"Laptop cost: ${laptop.get_total_cost()}")
print(f"Laptop price: {laptop.price}, quantity: {laptop.quantity}")

# ❌ Trying to set an invalid price (negative)
try:
phone = Product(price=-500, quantity=1)
except ValueError as e:
print(f"nError creating phone: {e}")

# ❌ Trying to set an invalid quantity (not a number)
try:
tablet = Product(price=300, quantity="one")
except TypeError as e:
print(f"nError creating tablet: {e}")

# 🔄 Modifying an existing attribute with invalid value
try:
laptop.price = 0
except ValueError as e:
print(f"nError modifying laptop price: {e}")

# 🗑️ Deleting an attribute
del laptop.quantity
try:
print(laptop.quantity)
except AttributeError as e:
print(f"nError accessing deleted quantity: {e}")
```

**Output:**
```
Laptop cost: $2401.0
Laptop price: 1200.5, quantity: 2

Error creating phone: 'price' must be positive.

Error creating tablet: 'quantity' must be a number.

Error modifying laptop price: 'price' must be positive.

Error accessing deleted quantity: '_quantity'
```

---

💡 **Small Challenge:**
Enhance the `ValidatedNumber` descriptor! Modify it so that it can accept an optional `max_value` argument during its creation (e.g., `price = ValidatedNumber(max_value=5000)`). Ensure that the set value is always less than or equal to `max_value` if it's provided. Good luck! 🚀

---
Which special method in a descriptor class is primarily responsible for receiving the name of the attribute it's assigned to in the owner class?
Anonymous Quiz
0%
A. `__get__`
100%
B. `__set__`
0%
C. `__set_name__`
0%
D. `__init__`
Which of the following special methods is NOT a core part of Python's descriptor protocol for controlling attribute access?
Anonymous Quiz
0%
__get__
0%
__set__
0%
__call__
0%
__delete__